This commit is contained in:
Daniil Chemerkin 2024-04-16 16:58:07 +00:00
parent f75ddb4e9c
commit f1254d9c2e
42 changed files with 1778 additions and 437 deletions

View File

@ -26,7 +26,9 @@ import {
AIRequestsV2, AIRequestsV2,
Assistants, Assistants,
OpenAI, OpenAI,
SinglePayment SinglePayment,
Products,
Palmistry,
} from './resources' } from './resources'
const api = { const api = {
@ -69,7 +71,9 @@ const api = {
getListRuns: createMethod<OpenAI.PayloadGetListRuns, OpenAI.ResponseGetListRuns>(OpenAI.createRequest), getListRuns: createMethod<OpenAI.PayloadGetListRuns, OpenAI.ResponseGetListRuns>(OpenAI.createRequest),
// Single payment // Single payment
getSinglePaymentProducts: createMethod<SinglePayment.PayloadGet, SinglePayment.ResponseGet[]>(SinglePayment.createRequestGet), getSinglePaymentProducts: createMethod<SinglePayment.PayloadGet, SinglePayment.ResponseGet[]>(SinglePayment.createRequestGet),
createSinglePayment: createMethod<SinglePayment.PayloadPost, SinglePayment.ResponsePost | SinglePayment.ResponsePostExistPaymentData>(SinglePayment.createRequestPost), createSinglePayment: createMethod<SinglePayment.PayloadPost, SinglePayment.ResponsePost>(SinglePayment.createRequestPost),
checkProductPurchased: createMethod<Products.PayloadGet, Products.ResponseGet>(Products.createRequest),
getPalmistryLines: createMethod<Palmistry.Payload, Palmistry.Response>(Palmistry.createRequest),
} }
export type ApiContextValue = typeof api export type ApiContextValue = typeof api

View File

@ -0,0 +1,26 @@
import routes from "@/routes";
export interface Payload {
formData: FormData;
}
export type Response = IPalmistryLine[];
export interface IPalmistryLine {
line: string;
points: IPalmistryPoint[];
}
export interface IPalmistryPoint {
x: number;
y: number;
}
export const createRequest = ({ formData }: Payload) => {
const url = new URL(routes.server.getPalmistryLines());
const body = formData;
return new Request(url, {
method: "POST",
body,
});
};

View File

@ -0,0 +1,29 @@
import routes from "@/routes";
import { getAuthHeaders } from "../utils";
interface Payload {
token: string;
}
export interface PayloadGet extends Payload {
productKey: string;
email: string;
}
interface ResponseGetSuccess {
status: string;
type: string;
active: boolean;
}
interface ResponseGetError {
status: string;
message: string;
}
export type ResponseGet = ResponseGetSuccess | ResponseGetError;
export const createRequest = ({ token, productKey, email }: PayloadGet): Request => {
const url = new URL(routes.server.dApiCheckProductPurchased(productKey, email));
return new Request(url, { method: "GET", headers: getAuthHeaders(token) });
};

View File

@ -7,6 +7,11 @@ interface Payload {
export type PayloadGet = Payload; export type PayloadGet = Payload;
export interface IPaymentInfo {
productId: string;
key: string;
}
export interface PayloadPost extends Payload { export interface PayloadPost extends Payload {
data: { data: {
user: { user: {
@ -21,9 +26,7 @@ export interface PayloadPost extends Payload {
sign: string | null; sign: string | null;
age: number | null; age: number | null;
}; };
paymentInfo: { paymentInfo: IPaymentInfo;
productId: string;
};
return_url: string; return_url: string;
}; };
} }
@ -35,7 +38,7 @@ export interface ResponseGet {
currency: string; currency: string;
} }
export interface ResponsePost { interface ResponsePostNewPaymentData {
paymentIntent: { paymentIntent: {
status: string; status: string;
data: { data: {
@ -57,13 +60,23 @@ export interface ResponsePost {
}; };
} }
export interface ResponsePostExistPaymentData { interface ResponsePostExistPaymentData {
payment: { payment: {
status: string; status: string;
invoiceId: string; invoiceId: string;
}; };
} }
interface ResponsePostError {
status: string;
message: string;
}
export type ResponsePost =
| ResponsePostNewPaymentData
| ResponsePostExistPaymentData
| ResponsePostError;
export const createRequestPost = ({ data, token }: PayloadPost): Request => { export const createRequestPost = ({ data, token }: PayloadPost): Request => {
const url = new URL(routes.server.dApiPaymentCheckout()); const url = new URL(routes.server.dApiPaymentCheckout());
const body = JSON.stringify(data); const body = JSON.stringify(data);

View File

@ -25,3 +25,5 @@ export * as AIRequestsV2 from "./AIRequestsV2";
export * as Assistants from "./Assistants"; export * as Assistants from "./Assistants";
export * as OpenAI from "./OpenAI"; export * as OpenAI from "./OpenAI";
export * as SinglePayment from "./SinglePayment"; export * as SinglePayment from "./SinglePayment";
export * as Products from "./Products";
export * as Palmistry from "./Palmistry";

View File

@ -1,4 +1,11 @@
import { useCallback, useEffect, useMemo, useState } from "react"; import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { import {
Routes, Routes,
Route, Route,
@ -109,7 +116,11 @@ import AdvisorChatPage from "../pages/AdvisorChat";
import PaymentWithEmailPage from "../pages/PaymentWithEmailPage"; import PaymentWithEmailPage from "../pages/PaymentWithEmailPage";
import SuccessPaymentPage from "../pages/PaymentWithEmailPage/ResultPayment/SuccessPaymentPage"; import SuccessPaymentPage from "../pages/PaymentWithEmailPage/ResultPayment/SuccessPaymentPage";
import FailPaymentPage from "../pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage"; import FailPaymentPage from "../pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage";
import { useSchemeColorByElement } from "@/hooks/useSchemeColorByElement";
import GetInformationPartnerPage from "../pages/GetInformationPartner"; import GetInformationPartnerPage from "../pages/GetInformationPartner";
import BirthPlacePage from "../pages/BirthPlacePage";
import LoadingPage from "../pages/LoadingPage";
import { EProductKeys, productUrls } from "@/data/products";
const isProduction = import.meta.env.MODE === "production"; const isProduction = import.meta.env.MODE === "production";
@ -124,9 +135,31 @@ function App(): JSX.Element {
const navigate = useNavigate(); const navigate = useNavigate();
const api = useApi(); const api = useApi();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { token, user, signUp } = useAuth(); const { token, user, signUp, logout } = useAuth();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const jwtToken = searchParams.get("token"); const jwtToken = searchParams.get("token");
const isForce = searchParams.get("force");
useEffect(() => {
if (isForce === "true") {
dispatch(actions.userConfig.addIsForceShortPath(true));
} else {
dispatch(actions.userConfig.addIsForceShortPath(false));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const isForceShortPath = useSelector(selectors.selectIsForceShortPath);
const { gender: genderFromStore } = useSelector(
selectors.selectQuestionnaire
);
const birthdateFromStore = useSelector(selectors.selectBirthdate);
const { birthPlace: birthPlaceFromStore } = useSelector(
selectors.selectQuestionnaire
);
const gender = user?.profile?.gender || genderFromStore;
const birthdate = user?.profile?.birthday || birthdateFromStore;
const birthPlace = user?.profile?.birthplace || birthPlaceFromStore;
useEffect(() => { useEffect(() => {
// api.getAppConfig({ bundleId: "auraweb" }), // api.getAppConfig({ bundleId: "auraweb" }),
@ -161,16 +194,17 @@ function App(): JSX.Element {
const { data } = useApiCall<Asset[]>(assetsData); const { data } = useApiCall<Asset[]>(assetsData);
// jwt auth // jwt auth
useEffect(() => { useLayoutEffect(() => {
(async () => { (async () => {
if (!jwtToken) return; if (!jwtToken) return;
logout();
const auth = await api.auth({ jwt: jwtToken }); const auth = await api.auth({ jwt: jwtToken });
const { const {
auth: { token, user }, auth: { token, user },
} = auth; } = auth;
signUp(token, user); signUp(token, user);
})(); })();
}, [api, jwtToken, signUp]); }, [api, jwtToken, logout, signUp]);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@ -231,23 +265,238 @@ function App(): JSX.Element {
return ( return (
<Routes> <Routes>
<Route element={<Layout setIsSpecialOfferOpen={setIsSpecialOfferOpen} />}> <Route element={<Layout setIsSpecialOfferOpen={setIsSpecialOfferOpen} />}>
<Route path={routes.client.loadingPage()} element={<LoadingPage />} />
{/* Email - Pay - Email */} {/* Email - Pay - Email */}
<Route path={routes.client.epeGender()} element={<GenderPage />} />
<Route path={routes.client.epeBirthdate()} element={<BirthdayPage />} />
<Route <Route
path={routes.client.epePayment()} element={
element={<PaymentWithEmailPage />} <ShortPathOutlet
/> productKey={EProductKeys["moons.pdf.aura"]}
redirectUrls={{
user: {
force: routes.client.epeBirthdate(),
},
purchasedProduct: {
no: routes.client.singlePaymentShortPath("moons.pdf.aura"),
},
}}
requiredParameters={[]}
/>
}
>
<Route
path={routes.client.epeGender()}
element={<GenderPage productKey={EProductKeys["moons.pdf.aura"]} />}
/>
</Route>
<Route <Route
path={routes.client.epeSuccessPayment()} element={
element={<SuccessPaymentPage />} <ShortPathOutlet
/> productKey={EProductKeys["moons.pdf.aura"]}
redirectUrls={{
data: {
no: routes.client.epeGender(),
},
purchasedProduct: {
no: routes.client.singlePaymentShortPath("moons.pdf.aura"),
},
}}
requiredParameters={[isForceShortPath || gender]}
/>
}
>
<Route
path={routes.client.epeBirthdate()}
element={<BirthdayPage />}
/>
</Route>
<Route
element={
<ShortPathOutlet
productKey={EProductKeys["moons.pdf.aura"]}
redirectUrls={{
user: {
no: routes.client.epeGender(),
force: routes.client.epeBirthdate(),
},
data: {
no: routes.client.epeGender(),
force: routes.client.epeBirthdate(),
},
purchasedProduct: {
no: routes.client.singlePaymentShortPath("moons.pdf.aura"),
},
}}
requiredParameters={[isForceShortPath || gender, birthdate]}
isProductPage={true}
/>
}
>
<Route
path={routes.client.epeSuccessPayment()}
element={<SuccessPaymentPage />}
/>
</Route>
<Route <Route
path={routes.client.epeFailPayment()} path={routes.client.epeFailPayment()}
element={<FailPaymentPage />} element={<FailPaymentPage />}
/> />
{/* Email - Pay - Email */} {/* Email - Pay - Email */}
{/* Advisor short path */}
<Route
element={
<ShortPathOutlet
productKey={EProductKeys["chat.aura"]}
redirectUrls={{
user: {
force: routes.client.advisorChatBirthdate(),
},
purchasedProduct: {
no: routes.client.singlePaymentShortPath("chat.aura"),
},
}}
requiredParameters={[]}
/>
}
>
<Route
path={routes.client.advisorChatGender()}
element={<GenderPage productKey={EProductKeys["chat.aura"]} />}
/>
</Route>
<Route
element={
<ShortPathOutlet
productKey={EProductKeys["chat.aura"]}
redirectUrls={{
data: {
no: routes.client.advisorChatGender(),
},
purchasedProduct: {
no: routes.client.singlePaymentShortPath("chat.aura"),
},
}}
requiredParameters={[isForceShortPath || gender]}
/>
}
>
<Route
path={routes.client.advisorChatBirthdate()}
element={<BirthdayPage />}
/>
</Route>
<Route
element={
<ShortPathOutlet
productKey={EProductKeys["chat.aura"]}
redirectUrls={{
data: {
no: routes.client.advisorChatGender(),
force: routes.client.advisorChatBirthdate(),
},
purchasedProduct: {
no: routes.client.singlePaymentShortPath("chat.aura"),
},
}}
requiredParameters={[birthdate, isForceShortPath || gender]}
/>
}
>
<Route
path={routes.client.advisorChatBirthtime()}
element={<BirthtimePage />}
/>
</Route>
<Route
element={
<ShortPathOutlet
productKey={EProductKeys["chat.aura"]}
redirectUrls={{
data: {
no: routes.client.advisorChatGender(),
force: routes.client.advisorChatBirthdate(),
},
purchasedProduct: {
no: routes.client.singlePaymentShortPath("chat.aura"),
},
}}
requiredParameters={[birthdate, isForceShortPath || gender]}
/>
}
>
<Route
path={routes.client.advisorChatBirthPlace()}
element={<BirthPlacePage />}
/>
</Route>
<Route
path={routes.client.advisorChatSuccessPayment()}
element={<SuccessPaymentPage />}
/>
<Route
path={routes.client.advisorChatFailPayment()}
element={<FailPaymentPage />}
/>
<Route
element={
<ShortPathOutlet
productKey={EProductKeys["chat.aura"]}
redirectUrls={{
user: {
no: routes.client.advisorChatGender(),
force: routes.client.advisorChatBirthdate(),
},
data: {
no: routes.client.advisorChatGender(),
force: routes.client.advisorChatBirthdate(),
},
purchasedProduct: {
no: routes.client.singlePaymentShortPath("chat.aura"),
},
}}
requiredParameters={[
birthdate,
birthPlace,
isForceShortPath || gender,
]}
isProductPage={true}
/>
}
>
<Route path={`${routes.client.advisorChatPrivate()}`}>
<Route path=":id" element={<AdvisorChatPage />} />
</Route>
</Route>
{/* Advisor short path */}
{/* Single Payment Page Short Path */}
<Route
element={
<ShortPathOutlet
productKey={EProductKeys["chat.aura"]}
redirectUrls={{
data: {
no: routes.client.advisorChatGender(),
force: routes.client.advisorChatBirthdate(),
},
}}
requiredParameters={[
birthdate,
birthPlace,
isForceShortPath || gender,
]}
/>
}
>
<Route
path={routes.client.singlePaymentShortPath()}
element={<PaymentWithEmailPage />}
>
<Route path=":productId" element={<PaymentWithEmailPage />} />
</Route>
</Route>
{/* Single Payment Page Short Path */}
{/* Test Routes Start */} {/* Test Routes Start */}
<Route path={routes.client.notFound()} element={<NotFoundPage />} /> <Route path={routes.client.notFound()} element={<NotFoundPage />} />
<Route path={routes.client.gender()} element={<GenderPage />}> <Route path={routes.client.gender()} element={<GenderPage />}>
@ -550,6 +799,10 @@ function Layout({ setIsSpecialOfferOpen }: LayoutProps): JSX.Element {
const changeIsSpecialOfferOpen = () => setIsSpecialOfferOpen(true); const changeIsSpecialOfferOpen = () => setIsSpecialOfferOpen(true);
const homeConfig = useSelector(selectors.selectHome); const homeConfig = useSelector(selectors.selectHome);
const showNavbarFooter = homeConfig.isShowNavbar; const showNavbarFooter = homeConfig.isShowNavbar;
const mainRef = useRef<HTMLDivElement>(null);
useSchemeColorByElement(mainRef.current, "section.page, .page, section", [
location,
]);
const birthdate = useSelector(selectors.selectBirthdate); const birthdate = useSelector(selectors.selectBirthdate);
const dataItems = useMemo(() => [birthdate], [birthdate]); const dataItems = useMemo(() => [birthdate], [birthdate]);
@ -644,7 +897,7 @@ function Layout({ setIsSpecialOfferOpen }: LayoutProps): JSX.Element {
<FullDataModal onClose={onCloseFullDataModal} /> <FullDataModal onClose={onCloseFullDataModal} />
</Modal> </Modal>
)} )}
<main className="content"> <main className="content" ref={mainRef}>
<Outlet /> <Outlet />
</main> </main>
{showFooter ? <Footer color={showNavbar ? "black" : "white"} /> : null} {showFooter ? <Footer color={showNavbar ? "black" : "white"} /> : null}
@ -658,6 +911,133 @@ function Layout({ setIsSpecialOfferOpen }: LayoutProps): JSX.Element {
); );
} }
// enum EIsAuthPageType {
// private,
// public,
// }
// interface ICheckIsAuthOutletProps {
// redirectUrl: string;
// pageType: EIsAuthPageType;
// }
// function CheckIsAuthOutlet({
// redirectUrl,
// pageType,
// }: ICheckIsAuthOutletProps): JSX.Element {
// const { user } = useAuth();
// if (user && pageType === EIsAuthPageType.public) {
// return <Navigate to={redirectUrl} replace={true} />;
// }
// if (!user && pageType === EIsAuthPageType.private) {
// return <Navigate to={redirectUrl} replace={true} />;
// }
// return <Outlet />;
// }
interface IShortPathOutletProps {
productKey: EProductKeys;
requiredParameters: unknown[];
isProductPage?: boolean;
redirectUrls: {
user?: {
yes?: string;
no?: string;
force?: string;
};
data?: {
yes?: string;
no?: string;
force?: string;
};
purchasedProduct?: {
yes?: string;
no?: string;
force?: string;
};
force?: {
yes?: string;
no?: string;
force?: string;
};
};
}
function ShortPathOutlet(props: IShortPathOutletProps): JSX.Element {
const { productKey, requiredParameters, redirectUrls, isProductPage } = props;
const { user, token } = useAuth();
const api = useApi();
const isForce = useSelector(selectors.selectIsForceShortPath);
const loadData = useCallback(async () => {
if (!token?.length || !user?.email || !productKey?.length)
return {
status: "error",
error: "Missing params",
};
try {
const purchased = await api.checkProductPurchased({
email: user?.email || "",
productKey,
token,
});
return purchased;
} catch (error) {
console.error(error);
return {
status: "error",
error: "Something went wrong",
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { data, isPending } = useApiCall(loadData);
if (isPending) {
return <LoadingPage />;
}
const isPurchasedProduct = !!(data && "active" in data && data.active);
const isUser = !!user && !!token.length;
const isFullData = requiredParameters.every((item) => !!item);
if (!isFullData) {
if (isForce && redirectUrls.data?.force) {
return <Navigate to={redirectUrls.data.force} replace={true} />;
}
if (redirectUrls.data?.no && !isForce) {
return <Navigate to={redirectUrls.data.no} replace={true} />;
}
return <Outlet />;
}
if (!isUser) {
if (isForce && redirectUrls.user?.force) {
return <Navigate to={redirectUrls.user.force} replace={true} />;
}
if (redirectUrls.user?.no && !isForce) {
return <Navigate to={redirectUrls.user.no} replace={true} />;
}
return <Outlet />;
}
if (!isPurchasedProduct) {
if (isForce && redirectUrls.purchasedProduct?.force) {
return (
<Navigate to={redirectUrls.purchasedProduct.force} replace={true} />
);
}
if (redirectUrls.purchasedProduct?.no && !isForce) {
return <Navigate to={redirectUrls.purchasedProduct.no} replace={true} />;
}
return <Outlet />;
}
if (isProductPage) {
return <Outlet />;
}
return <Navigate to={productUrls[productKey]} replace={true} />;
}
function AuthorizedUserOutlet(): JSX.Element { function AuthorizedUserOutlet(): JSX.Element {
const status = useSelector(selectors.selectStatus); const status = useSelector(selectors.selectStatus);
const { user } = useAuth(); const { user } = useAuth();

View File

@ -29,3 +29,12 @@
.page-responsive { .page-responsive {
width: 100%; width: 100%;
} }
#color-pointer {
position: fixed;
background: transparent;
z-index: 12323423;
width: 100%;
height: 2px;
pointer-events: none;
}

View File

@ -17,9 +17,13 @@ function BirthdayPage(): JSX.Element {
const navigate = useNavigate(); const navigate = useNavigate();
const birthdate = useSelector(selectors.selectBirthdate); const birthdate = useSelector(selectors.selectBirthdate);
const [isDisabled, setIsDisabled] = useState(true); const [isDisabled, setIsDisabled] = useState(true);
const nextRoute = window.location.href.includes("/epe/") let nextRoute = routes.client.didYouKnow();
? routes.client.epePayment() if (window.location.href.includes("/epe/")) {
: routes.client.didYouKnow(); nextRoute = routes.client.singlePaymentShortPath("moons.pdf.aura");
}
if (window.location.href.includes("/advisor-chat/")) {
nextRoute = routes.client.advisorChatBirthtime();
}
const handleNext = () => navigate(nextRoute); const handleNext = () => navigate(nextRoute);
const handleValid = (birthdate: string) => { const handleValid = (birthdate: string) => {
dispatch(actions.form.addDate(birthdate)); dispatch(actions.form.addDate(birthdate));

View File

@ -1,28 +1,34 @@
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom";
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from '@/store' import { actions, selectors } from "@/store";
import { TimePicker } from "../DateTimePicker" import { TimePicker } from "../DateTimePicker";
import Title from "../Title" import Title from "../Title";
import MainButton from "../MainButton" import MainButton from "../MainButton";
import routes from "@/routes" import routes from "@/routes";
import './styles.css' import "./styles.css";
function BirthtimePage(): JSX.Element { function BirthtimePage(): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation();
const dispatch = useDispatch() const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const birthtime = useSelector(selectors.selectBirthtime) const birthtime = useSelector(selectors.selectBirthtime);
const handleNext = () => navigate(routes.client.createProfile()) let nextRoute = routes.client.createProfile();
const handleChange = (value: string) => dispatch(actions.form.addTime(value)) if (window.location.href.includes("/advisor-chat/")) {
nextRoute = routes.client.advisorChatBirthPlace();
}
const handleNext = () => navigate(nextRoute);
const handleChange = (value: string) => dispatch(actions.form.addTime(value));
return ( return (
<section className='page'> <section className="page">
<Title variant="h2" className="mt-24">{t('born_time_question')}</Title> <Title variant="h2" className="mt-24">
<p className="description">{t('nasa_data_using')}</p> {t("born_time_question")}
<TimePicker value={birthtime} onChange={handleChange}/> </Title>
<MainButton onClick={handleNext}>{t('next')}</MainButton> <p className="description">{t("nasa_data_using")}</p>
<TimePicker value={birthtime} onChange={handleChange} />
<MainButton onClick={handleNext}>{t("next")}</MainButton>
</section> </section>
) );
} }
export default BirthtimePage export default BirthtimePage;

View File

@ -1,5 +1,4 @@
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import routes from "@/routes";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
@ -8,6 +7,7 @@ import { actions } from "@/store";
// import { useAuth } from "@/auth"; // import { useAuth } from "@/auth";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import Loader from "@/components/Loader"; import Loader from "@/components/Loader";
import { paymentResultPathsOfProducts } from "@/data/products";
function PaymentResultPage(): JSX.Element { function PaymentResultPage(): JSX.Element {
// const api = useApi(); // const api = useApi();
@ -16,7 +16,7 @@ function PaymentResultPage(): JSX.Element {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const status = searchParams.get("redirect_status"); const status = searchParams.get("redirect_status");
const type = searchParams.get("type"); const redirect_type = searchParams.get("redirect_type");
// const { id } = useParams(); // const { id } = useParams();
// const requestTimeOutRef = useRef<NodeJS.Timeout>(); // const requestTimeOutRef = useRef<NodeJS.Timeout>();
const [isLoading] = useState(true); const [isLoading] = useState(true);
@ -90,18 +90,19 @@ function PaymentResultPage(): JSX.Element {
useEffect(() => { useEffect(() => {
if (status === "succeeded") { if (status === "succeeded") {
dispatch(actions.status.update("subscribed")); dispatch(actions.status.update("subscribed"));
let successPaymentRoute = routes.client.paymentSuccess(); if (
if (type === "epe") { !paymentResultPathsOfProducts[redirect_type || ""] ||
successPaymentRoute = routes.client.epeSuccessPayment(); !redirect_type
) {
return navigate(paymentResultPathsOfProducts.default.success);
} }
return navigate(successPaymentRoute); return navigate(paymentResultPathsOfProducts[redirect_type].success);
} }
let failPaymentRoute = routes.client.paymentFail(); if (!paymentResultPathsOfProducts[redirect_type || ""] || !redirect_type) {
if (type === "epe") { return navigate(paymentResultPathsOfProducts.default.fail);
failPaymentRoute = routes.client.epeFailPayment();
} }
return navigate(failPaymentRoute); return navigate(paymentResultPathsOfProducts[redirect_type].fail);
}, [navigate, status, dispatch]); }, [navigate, status, dispatch, redirect_type]);
return <div className={styles.page}>{isLoading && <Loader />}</div>; return <div className={styles.page}>{isLoading && <Loader />}</div>;
} }

View File

@ -12,7 +12,6 @@ import { selectors } from "@/store";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { import {
ResponsePost, ResponsePost,
ResponsePostExistPaymentData,
} from "@/api/resources/SinglePayment"; } from "@/api/resources/SinglePayment";
import { createSinglePayment } from "@/services/singlePayment"; import { createSinglePayment } from "@/services/singlePayment";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
@ -27,7 +26,7 @@ function AddConsultationPage() {
const tokenFromStore = useSelector(selectors.selectToken); const tokenFromStore = useSelector(selectors.selectToken);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [paymentIntent, setPaymentIntent] = useState< const [paymentIntent, setPaymentIntent] = useState<
ResponsePost | ResponsePostExistPaymentData | null ResponsePost | null
>(null); >(null);
const [isError, setIsError] = useState(false); const [isError, setIsError] = useState(false);
const returnUrl = `${window.location.protocol}//${ const returnUrl = `${window.location.protocol}//${
@ -48,9 +47,14 @@ function AddConsultationPage() {
const handleClick = async () => { const handleClick = async () => {
if (!userFromStore || !currentProduct) return; if (!userFromStore || !currentProduct) return;
setIsLoading(true); setIsLoading(true);
const { productId, key } = currentProduct;
const paymentInfo = {
productId,
key,
};
const paymentIntent = await createSinglePayment( const paymentIntent = await createSinglePayment(
userFromStore, userFromStore,
currentProduct.productId, paymentInfo,
tokenFromStore, tokenFromStore,
userFromStore.email, userFromStore.email,
userFromStore.profile.full_name, userFromStore.profile.full_name,

View File

@ -11,7 +11,6 @@ import PaymentAddress from "../../components/PaymentAddress";
import { createSinglePayment } from "@/services/singlePayment"; import { createSinglePayment } from "@/services/singlePayment";
import { import {
ResponsePost, ResponsePost,
ResponsePostExistPaymentData,
} from "@/api/resources/SinglePayment"; } from "@/api/resources/SinglePayment";
import { useAuth } from "@/auth"; import { useAuth } from "@/auth";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
@ -28,7 +27,7 @@ function AddReportPage() {
const api = useApi(); const api = useApi();
const tokenFromStore = useSelector(selectors.selectToken); const tokenFromStore = useSelector(selectors.selectToken);
const [paymentIntent, setPaymentIntent] = useState< const [paymentIntent, setPaymentIntent] = useState<
ResponsePost | ResponsePostExistPaymentData | null ResponsePost | null
>(null); >(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false); const [isError, setIsError] = useState(false);
@ -53,9 +52,14 @@ function AddReportPage() {
const currentProduct = getCurrentProduct(activeOffer?.productKey); const currentProduct = getCurrentProduct(activeOffer?.productKey);
if (!currentProduct) return; if (!currentProduct) return;
setIsLoading(true); setIsLoading(true);
const { productId, key } = currentProduct;
const paymentInfo = {
productId,
key,
};
const paymentIntent = await createSinglePayment( const paymentIntent = await createSinglePayment(
userFromStore, userFromStore,
currentProduct.productId, paymentInfo,
tokenFromStore, tokenFromStore,
userFromStore.email, userFromStore.email,
userFromStore.profile.full_name, userFromStore.profile.full_name,

View File

@ -18,7 +18,6 @@ import { createSinglePayment } from "@/services/singlePayment";
import Loader, { LoaderColor } from "@/components/Loader"; import Loader, { LoaderColor } from "@/components/Loader";
import { import {
ResponsePost, ResponsePost,
ResponsePostExistPaymentData,
} from "@/api/resources/SinglePayment"; } from "@/api/resources/SinglePayment";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import { getPriceCentsToDollars } from "@/services/price"; import { getPriceCentsToDollars } from "@/services/price";
@ -42,7 +41,7 @@ function UnlimitedReadingsPage() {
const tokenFromStore = useSelector(selectors.selectToken); const tokenFromStore = useSelector(selectors.selectToken);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [paymentIntent, setPaymentIntent] = useState< const [paymentIntent, setPaymentIntent] = useState<
ResponsePost | ResponsePostExistPaymentData | null ResponsePost | null
>(null); >(null);
const [isError, setIsError] = useState(false); const [isError, setIsError] = useState(false);
const returnUrl = `${window.location.protocol}//${ const returnUrl = `${window.location.protocol}//${
@ -63,9 +62,14 @@ function UnlimitedReadingsPage() {
const handleClick = async () => { const handleClick = async () => {
if (!userFromStore || !currentProduct) return; if (!userFromStore || !currentProduct) return;
setIsLoading(true); setIsLoading(true);
const { productId, key } = currentProduct;
const paymentInfo = {
productId,
key,
};
const paymentIntent = await createSinglePayment( const paymentIntent = await createSinglePayment(
userFromStore, userFromStore,
currentProduct.productId, paymentInfo,
tokenFromStore, tokenFromStore,
userFromStore.email, userFromStore.email,
userFromStore.profile.full_name, userFromStore.profile.full_name,

View File

@ -4,6 +4,7 @@ interface IChatHeaderProps {
name: string; name: string;
avatar: string; avatar: string;
classNameContainer?: string; classNameContainer?: string;
hasBackButton?: boolean;
clickBackButton: () => void; clickBackButton: () => void;
} }
@ -11,13 +12,16 @@ function ChatHeader({
name, name,
avatar, avatar,
classNameContainer = "", classNameContainer = "",
hasBackButton = true,
clickBackButton, clickBackButton,
}: IChatHeaderProps) { }: IChatHeaderProps) {
return ( return (
<div className={`${styles.container} ${classNameContainer}`}> <div className={`${styles.container} ${classNameContainer}`}>
<div className={styles["back-button"]} onClick={clickBackButton}> {hasBackButton && (
<div className={styles["arrow"]} /> Advisors <div className={styles["back-button"]} onClick={clickBackButton}>
</div> <div className={styles["arrow"]} /> Advisors
</div>
)}
<div className={styles.name}> <div className={styles.name}>
{name} {name}
<span className={styles["online-status"]} /> <span className={styles["online-status"]} />

View File

@ -13,14 +13,24 @@ import { IMessage, ResponseRunThread } from "@/api/resources/OpenAI";
import Message from "./components/Message"; import Message from "./components/Message";
import LoaderDots from "./components/LoaderDots"; import LoaderDots from "./components/LoaderDots";
import useDetectScroll from "@smakss/react-scroll-direction"; import useDetectScroll from "@smakss/react-scroll-direction";
import { getZodiacSignByDate } from "@/services/zodiac-sign";
import { useAuth } from "@/auth";
function AdvisorChatPage() { function AdvisorChatPage() {
const isPrivateChat = window.location.href.includes("/advisor-chat-private/");
const { id } = useParams(); const { id } = useParams();
const api = useApi(); const api = useApi();
const navigate = useNavigate(); const navigate = useNavigate();
const { scrollDir, scrollPosition } = useDetectScroll(); const { scrollDir, scrollPosition } = useDetectScroll();
const token = useSelector(selectors.selectToken); const token = useSelector(selectors.selectToken);
const openAiToken = useSelector(selectors.selectOpenAiToken); const openAiToken = useSelector(selectors.selectOpenAiToken);
const birthdate = useSelector(selectors.selectBirthdate);
const zodiacSign = getZodiacSignByDate(birthdate);
const { username } = useSelector(selectors.selectUser);
const { gender, birthtime, birthPlace } = useSelector(
selectors.selectQuestionnaire
);
const { user } = useAuth();
const [assistant, setAssistant] = useState<IAssistant>(); const [assistant, setAssistant] = useState<IAssistant>();
const [messageText, setMessageText] = useState(""); const [messageText, setMessageText] = useState("");
const [textareaRows, setTextareaRows] = useState(1); const [textareaRows, setTextareaRows] = useState(1);
@ -83,7 +93,7 @@ function AdvisorChatPage() {
idAssistant: string | number idAssistant: string | number
) => { ) => {
const currentAssistant = aiAssistants.find( const currentAssistant = aiAssistants.find(
(a) => a.id === Number(idAssistant) (a) => a.external_id === idAssistant
); );
return currentAssistant; return currentAssistant;
}; };
@ -101,7 +111,7 @@ function AdvisorChatPage() {
const setExternalChatIdAssistant = async (threadId: string) => { const setExternalChatIdAssistant = async (threadId: string) => {
await api.setExternalChatIdAssistant({ await api.setExternalChatIdAssistant({
token, token,
chatId: String(id), chatId: String(assistant?.id || 1),
ai_assistant_chat: { ai_assistant_chat: {
external_id: threadId, external_id: threadId,
}, },
@ -191,13 +201,25 @@ function AdvisorChatPage() {
return run; return run;
}; };
const getContentMessage = (messageText: string) => {
const sign = zodiacSign || user?.profile?.sign || "unknown";
const _gender = gender || user?.profile?.gender || "unknown";
const _birthDate = birthdate || user?.profile?.birthday || "unknown";
const _birthtime = birthtime || "unknown";
const _birthPlace = birthPlace || user?.profile?.birthplace || "unknown";
const name = username || user?.profile?.full_name || "unknown";
const content = `#USER INFO: zodiac sign - ${sign}; gender - ${_gender}; birthdate - ${_birthDate}; name - ${name}; birthtime - ${_birthtime}; birthPlace - ${_birthPlace}# ${messageText}`;
return content;
};
const createMessage = async (messageText: string, threadId: string) => { const createMessage = async (messageText: string, threadId: string) => {
const content = getContentMessage(messageText);
const message = await api.createMessage({ const message = await api.createMessage({
token: openAiToken, token: openAiToken,
method: "POST", method: "POST",
path: routes.openAi.createMessage(threadId), path: routes.openAi.createMessage(threadId),
role: "user", role: "user",
content: messageText, content: content,
}); });
return message; return message;
}; };
@ -252,7 +274,8 @@ function AdvisorChatPage() {
threadId = assistant.external_chat_id; threadId = assistant.external_chat_id;
assistantId = assistant.external_id || ""; assistantId = assistant.external_id || "";
} else { } else {
const thread = await createThread(messageText); const content = getContentMessage(messageText);
const thread = await createThread(content);
threadId = thread.id; threadId = thread.id;
assistantId = assistant?.external_id || ""; assistantId = assistant?.external_id || "";
@ -294,6 +317,14 @@ function AdvisorChatPage() {
const getIsSelfMessage = (role: string) => role === "user"; const getIsSelfMessage = (role: string) => role === "user";
const deleteDataFromMessage = (messageText: string) => {
const splittedText = messageText.split("#");
if (splittedText.length > 2) {
return splittedText.slice(2).join("#");
}
return messageText;
};
return ( return (
<section className={`${styles.page} page`}> <section className={`${styles.page} page`}>
{isLoading && ( {isLoading && (
@ -305,6 +336,7 @@ function AdvisorChatPage() {
avatar={assistant?.photo?.th2x || ""} avatar={assistant?.photo?.th2x || ""}
classNameContainer={styles["header-container"]} classNameContainer={styles["header-container"]}
clickBackButton={() => navigate(-1)} clickBackButton={() => navigate(-1)}
hasBackButton={!isPrivateChat}
/> />
)} )}
{!!messages.length && ( {!!messages.length && (
@ -322,7 +354,7 @@ function AdvisorChatPage() {
message.content.map((content) => ( message.content.map((content) => (
<Message <Message
avatar={assistant?.photo?.th2x || ""} avatar={assistant?.photo?.th2x || ""}
text={content.text.value} text={deleteDataFromMessage(content.text.value)}
advisorName={assistant?.name || ""} advisorName={assistant?.name || ""}
backgroundTextColor={ backgroundTextColor={
getIsSelfMessage(message.role) ? "#0080ff" : "#c9c9c9" getIsSelfMessage(message.role) ? "#0080ff" : "#c9c9c9"

View File

@ -27,7 +27,7 @@ function AssistantCard({
</Title> </Title>
</div> </div>
<div className={styles.footer}> <div className={styles.footer}>
<span className={styles.expirience}>{expirience}</span> <span className={styles.experience}>{expirience}</span>
<div className={styles["rating-container"]}> <div className={styles["rating-container"]}>
<div className={styles.stars}> <div className={styles.stars}>
{Array(stars) {Array(stars)

View File

@ -70,10 +70,11 @@
margin-top: 8px; margin-top: 8px;
} }
.expirience { .experience {
white-space: nowrap; white-space: nowrap;
display: block; display: block;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
width: 100%; width: 100%;
line-height: 150%;
} }

View File

@ -25,13 +25,13 @@ function Advisors() {
setAssistants(ai_assistants); setAssistants(ai_assistants);
setIsLoading(false); setIsLoading(false);
return { ai_assistants }; return { ai_assistants };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [api, dispatch, token]); }, [api, dispatch, token]);
useApiCall<Assistants.Response>(loadData); useApiCall<Assistants.Response>(loadData);
const handleAdvisorClick = (assistant: IAssistant) => { const handleAdvisorClick = (assistant: IAssistant) => {
navigate(routes.client.advisorChat(assistant.id)); navigate(routes.client.advisorChat(assistant.external_id));
}; };
return ( return (

View File

@ -0,0 +1,48 @@
import Title from "@/components/Title";
import styles from "./styles.module.css";
import PlacePicker from "@/components/PlacePicker";
import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store";
import MainButton from "@/components/MainButton";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
function BirthPlacePage() {
const dispatch = useDispatch();
const { t } = useTranslation();
const navigate = useNavigate();
const { birthPlace } = useSelector(selectors.selectQuestionnaire);
const handleChange = (birthPlace: string) => {
return dispatch(actions.questionnaire.update({ birthPlace }));
};
const handleNext = () => {
navigate(routes.client.singlePaymentShortPath("chat.aura"));
};
return (
<section className={`${styles.page} page`}>
<Title variant="h1" className={styles.title}>
Where were you born?
</Title>
<p className={styles.description}>
Please select the city where you were born.
</p>
<PlacePicker
value={birthPlace}
name="birthPlace"
maxLength={1000}
onChange={handleChange}
/>
{!!birthPlace.length && (
<MainButton className={styles.button} onClick={handleNext}>
{t("next")}
</MainButton>
)}
</section>
);
}
export default BirthPlacePage;

View File

@ -0,0 +1,28 @@
.page {
height: fit-content;
min-height: calc(100dvh - 50px);
background-image: url(/bunch_of_cards.webp);
display: flex;
flex-direction: column;
align-items: center;
padding-top: 40px;
gap: 16px;
}
.title {
margin: 0;
}
/* .button {
background: linear-gradient(
165.54deg,
rgb(20, 19, 51) -33.39%,
rgb(32, 34, 97) 15.89%,
rgb(84, 60, 151) 55.84%,
rgb(105, 57, 162) 74.96%
);
min-height: 0;
height: 49px;
border-radius: 12px;
margin-top: 26px;
} */

View File

@ -1,17 +1,21 @@
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import Title from "@/components/Title"; import Title from "@/components/Title";
import { Gender, genders } from "@/data"; import { Gender, genders } from "@/data";
import { EProductKeys } from "@/data/products";
import routes from "@/routes"; import routes from "@/routes";
import { actions } from "@/store"; import { actions } from "@/store";
import { useEffect } from "react"; import { useEffect } from "react";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
function GenderPage(): JSX.Element { interface IGenderPageProps {
productKey?: EProductKeys;
}
function GenderPage({ productKey }: IGenderPageProps): JSX.Element {
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const { targetId } = useParams(); const { targetId } = useParams();
const pathName = window.location.pathname;
useEffect(() => { useEffect(() => {
const isShowTryApp = targetId === "i"; const isShowTryApp = targetId === "i";
@ -20,9 +24,12 @@ function GenderPage(): JSX.Element {
const selectGender = (gender: Gender) => { const selectGender = (gender: Gender) => {
dispatch(actions.questionnaire.update({ gender: gender.id })); dispatch(actions.questionnaire.update({ gender: gender.id }));
if (pathName.includes("/epe/gender")) { if (productKey === EProductKeys["moons.pdf.aura"]) {
return navigate(routes.client.epeBirthdate()); return navigate(routes.client.epeBirthdate());
} }
if (productKey === EProductKeys["chat.aura"]) {
return navigate(routes.client.advisorChatBirthdate());
}
navigate(`/questionnaire/profile/flowChoice`); navigate(`/questionnaire/profile/flowChoice`);
}; };

View File

@ -0,0 +1,12 @@
import Loader, { LoaderColor } from "@/components/Loader";
import styles from "./styles.module.css";
function LoadingPage() {
return (
<section className={`${styles.page} page`}>
<Loader color={LoaderColor.Black} />
</section>
);
}
export default LoadingPage;

View File

@ -0,0 +1,8 @@
.page {
height: fit-content;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

View File

@ -8,7 +8,12 @@ import MainButton from "@/components/MainButton";
function FailPaymentPage(): JSX.Element { function FailPaymentPage(): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const handleNext = () => navigate(routes.client.epePayment()); const isAdvisorChat = window.location.href.includes("/advisor-chat/");
let nextRoute = routes.client.epePayment();
if (isAdvisorChat) {
nextRoute = routes.client.advisorChatGender();
}
const handleNext = () => navigate(nextRoute);
return ( return (
<section className={`${styles.page} page`}> <section className={`${styles.page} page`}>

View File

@ -1,9 +1,17 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import Title from "@/components/Title"; import Title from "@/components/Title";
import MainButton from "@/components/MainButton";
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
function SuccessPaymentPage(): JSX.Element { function SuccessPaymentPage(): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate();
const isAdvisorChat = window.location.href.includes("/advisor-chat/");
const titleText = isAdvisorChat
? "The payment was successful"
: "The information has been sent to your email";
return ( return (
<section className={`${styles.page} page`}> <section className={`${styles.page} page`}>
@ -13,9 +21,21 @@ function SuccessPaymentPage(): JSX.Element {
style={{ minHeight: "98px" }} style={{ minHeight: "98px" }}
/> />
<div className={styles.text}> <div className={styles.text}>
<Title variant="h1">The information has been sent to your e-mail</Title> <Title variant="h1">{titleText}</Title>
<p>{t("auweb.pay_good.text1")}</p> <p>{t("auweb.pay_good.text1")}</p>
</div> </div>
{isAdvisorChat && (
<MainButton
className={styles.button}
onClick={() =>
navigate(
routes.client.advisorChatPrivate("asst_WWkAlT4Ovs6gKRy6VEn9LqNS")
)
}
>
{t("auweb.pay_good.button")}
</MainButton>
)}
</section> </section>
); );
} }

View File

@ -12,23 +12,18 @@ import { getClientTimezone } from "@/locales";
import ErrorText from "@/components/ErrorText"; import ErrorText from "@/components/ErrorText";
import Title from "@/components/Title"; import Title from "@/components/Title";
import NameInput from "@/components/EmailEnterPage/NameInput"; import NameInput from "@/components/EmailEnterPage/NameInput";
import { import { useParams } from "react-router-dom";
ResponseGet,
ResponsePost,
ResponsePostExistPaymentData,
} from "@/api/resources/SinglePayment";
import { useNavigate } from "react-router-dom";
import routes from "@/routes"; import routes from "@/routes";
import PaymentForm from "./PaymentForm"; import PaymentForm from "./PaymentForm";
import { getPriceCentsToDollars } from "@/services/price"; import { getPriceCentsToDollars } from "@/services/price";
import { createSinglePayment } from "@/services/singlePayment"; import { useSinglePayment } from "@/hooks/payment/useSinglePayment";
function PaymentWithEmailPage() { function PaymentWithEmailPage() {
const { productId } = useParams();
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const tokenFromStore = useSelector(selectors.selectToken); // const tokenFromStore = useSelector(selectors.selectToken);
const { signUp, user: userFromStore } = useAuth(); const { signUp, user: userFromStore, token: tokenFromStore } = useAuth();
const api = useApi(); const api = useApi();
const navigate = useNavigate();
const timezone = getClientTimezone(); const timezone = getClientTimezone();
const dispatch = useDispatch(); const dispatch = useDispatch();
const birthday = useSelector(selectors.selectBirthday); const birthday = useSelector(selectors.selectBirthday);
@ -37,26 +32,44 @@ function PaymentWithEmailPage() {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [name, setName] = useState(""); const [name, setName] = useState("");
const [isValidEmail, setIsValidEmail] = useState(false); const [isValidEmail, setIsValidEmail] = useState(false);
const [isValidName, setIsValidName] = useState(true); const [isValidName, setIsValidName] = useState(productId !== "chat.aura");
const [isDisabled, setIsDisabled] = useState(true); const [isDisabled, setIsDisabled] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [isLoadingPage, setIsLoadingPage] = useState(false);
const [isAuth, setIsAuth] = useState(false); const [isAuth, setIsAuth] = useState(false);
const [apiError, setApiError] = useState<ApiError | null>(null); const [apiError, setApiError] = useState<ApiError | null>(null);
const [error, setError] = useState<boolean>(false); const [error, setError] = useState<boolean>(false);
const [paymentIntent, setPaymentIntent] = useState< const returnUrl = `${window.location.protocol}//${
ResponsePost | ResponsePostExistPaymentData | null window.location.host
>(null); }${routes.client.paymentResult()}`;
const [currentProduct, setCurrentProduct] = useState<ResponseGet>();
const returnUrl = `${window.location.protocol}//${window.location.host}/payment/result/?type=epe`; const [isLoadingAuth, setIsLoadingAuth] = useState<boolean>(false);
const {
product,
paymentIntent,
createSinglePayment,
isLoading: isLoadingSinglePayment,
error: errorSinglePayment,
} = useSinglePayment();
useEffect(() => { useEffect(() => {
if (isValidName && isValidEmail) { if (
isValidName &&
isValidEmail &&
!(error || apiError || errorSinglePayment?.error)
) {
setIsDisabled(false); setIsDisabled(false);
} else { } else {
setIsDisabled(true); setIsDisabled(true);
} }
}, [isValidEmail, email, isValidName, name]); }, [
isValidEmail,
email,
isValidName,
name,
error,
apiError,
errorSinglePayment?.error,
]);
const handleValidEmail = (email: string) => { const handleValidEmail = (email: string) => {
dispatch(actions.form.addEmail(email)); dispatch(actions.form.addEmail(email));
@ -71,7 +84,7 @@ function PaymentWithEmailPage() {
const authorization = async () => { const authorization = async () => {
try { try {
setIsLoading(true); setIsLoadingAuth(true);
const auth = await api.auth({ email, timezone, locale }); const auth = await api.auth({ email, timezone, locale });
const { const {
auth: { token, user }, auth: { token, user },
@ -103,6 +116,7 @@ function PaymentWithEmailPage() {
dispatch(actions.status.update("registred")); dispatch(actions.status.update("registred"));
setIsAuth(true); setIsAuth(true);
const userUpdated = await api.getUser({ token }); const userUpdated = await api.getUser({ token });
setIsLoadingAuth(false);
return { user: userUpdated?.user, token }; return { user: userUpdated?.user, token };
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -111,19 +125,10 @@ function PaymentWithEmailPage() {
} else { } else {
setError(true); setError(true);
} }
setIsLoadingAuth(false);
} }
}; };
const getCurrentProduct = async (token: string) => {
const productsSinglePayment = await api.getSinglePaymentProducts({
token,
});
const currentProduct = productsSinglePayment.find(
(product) => product.key === "moons.pdf.aura"
);
return currentProduct;
};
const handleClick = async () => { const handleClick = async () => {
const authData = await authorization(); const authData = await authorization();
if (!authData) { if (!authData) {
@ -131,68 +136,32 @@ function PaymentWithEmailPage() {
} }
const { user, token } = authData; const { user, token } = authData;
const currentProduct = await getCurrentProduct(token); await createSinglePayment({
if (!currentProduct) {
setError(true);
return;
}
setCurrentProduct(currentProduct);
const { productId } = currentProduct;
const paymentIntent = await createSinglePayment(
user, user,
productId,
token, token,
email, targetProductKey: productId || "",
name,
birthday,
returnUrl, returnUrl,
api, });
gender
);
setPaymentIntent(paymentIntent);
setIsLoading(false);
if ("payment" in paymentIntent) {
if (paymentIntent.payment.status === "paid")
return navigate(routes.client.epeSuccessPayment());
return navigate(routes.client.epeFailPayment());
}
}; };
const handleAuthUser = useCallback(async () => { const handleAuthUser = useCallback(async () => {
if (!tokenFromStore.length || !userFromStore) { if (!tokenFromStore.length || !userFromStore) {
return; return;
} }
setIsLoadingPage(true);
const currentProduct = await getCurrentProduct(tokenFromStore);
if (!currentProduct) {
setError(true);
return;
}
setCurrentProduct(currentProduct);
const { productId } = currentProduct; await createSinglePayment({
const paymentIntent = await createSinglePayment( user: userFromStore,
userFromStore, token: tokenFromStore,
productId, targetProductKey: productId || "",
tokenFromStore,
userFromStore.email,
userFromStore.profile.full_name,
userFromStore.profile.birthday,
returnUrl, returnUrl,
api, });
gender }, [
); createSinglePayment,
setPaymentIntent(paymentIntent); productId,
setIsLoadingPage(false); returnUrl,
setIsLoading(false); tokenFromStore,
if ("payment" in paymentIntent) { userFromStore,
if (paymentIntent.payment.status === "paid") ]);
return navigate(routes.client.epeSuccessPayment());
return navigate(routes.client.epeFailPayment());
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => { useEffect(() => {
handleAuthUser(); handleAuthUser();
@ -201,60 +170,67 @@ function PaymentWithEmailPage() {
return ( return (
<div className={`${styles.page} page`}> <div className={`${styles.page} page`}>
{isLoadingPage && <Loader color={LoaderColor.Black} />} {(isLoadingSinglePayment || isLoadingSinglePayment) && (
{!isLoadingPage && <Loader color={LoaderColor.Black} />
)}
{!isLoadingSinglePayment &&
!isLoadingAuth &&
paymentIntent && paymentIntent &&
"paymentIntent" in paymentIntent && "paymentIntent" in paymentIntent &&
!!tokenFromStore.length && ( !!tokenFromStore.length && (
<> <>
<Title variant="h1" className={styles.title}> <Title variant="h1" className={styles.title}>
{getPriceCentsToDollars(currentProduct?.amount || 0)}$ {getPriceCentsToDollars(product?.amount || 0)}$
</Title> </Title>
<PaymentForm <PaymentForm
stripePublicKey={paymentIntent.paymentIntent.data.public_key} stripePublicKey={paymentIntent.paymentIntent.data.public_key}
clientSecret={paymentIntent.paymentIntent.data.client_secret} clientSecret={paymentIntent.paymentIntent.data.client_secret}
returnUrl={returnUrl} returnUrl={`${returnUrl}?redirect_type=${product?.key}`}
/> />
</> </>
)} )}
{(!tokenFromStore || !paymentIntent) && !isLoadingPage && ( {(!tokenFromStore || !paymentIntent) &&
<> // || (productId !== "chat.aura" && !name.length)
<NameInput !isLoadingSinglePayment &&
value={name} !isLoadingAuth && (
placeholder="Your name" <>
onValid={handleValidName} <NameInput
onInvalid={() => setIsValidName(true)} value={name}
/> placeholder="Your name"
<EmailInput onValid={handleValidName}
name="email" onInvalid={() => setIsValidName(productId !== "chat.aura")}
value={email} />
placeholder={t("your_email")} <EmailInput
onValid={handleValidEmail} name="email"
onInvalid={() => setIsValidEmail(false)} value={email}
/> placeholder={t("your_email")}
onValid={handleValidEmail}
onInvalid={() => setIsValidEmail(false)}
/>
<MainButton <MainButton
className={styles.button} className={styles.button}
onClick={handleClick} onClick={handleClick}
disabled={isDisabled} disabled={isDisabled}
> >
{isLoading && <Loader color={LoaderColor.White} />} {isLoadingSinglePayment && <Loader color={LoaderColor.White} />}
{!isLoading && {!isLoadingSinglePayment &&
!(!apiError && !error && !isLoading && isAuth) && !(!apiError && !error && !isLoadingSinglePayment && isAuth) &&
t("_continue")} t("_continue")}
{!apiError && !error && !isLoading && isAuth && ( {!apiError && !error && !isLoadingSinglePayment && isAuth && (
<img <img
className={styles["success-icon"]} className={styles["success-icon"]}
src="/SuccessIcon.png" src="/SuccessIcon.png"
alt="Success Icon" alt="Success Icon"
/> />
)} )}
</MainButton> </MainButton>
</> </>
)} )}
{(error || apiError) && ( {(error || apiError || errorSinglePayment?.error) && (
<Title variant="h3" style={{ color: "red", margin: 0 }}> <Title variant="h3" style={{ color: "red", margin: 0 }}>
Something went wrong Something went wrong:{" "}
{errorSinglePayment?.error?.length && errorSinglePayment?.error}
</Title> </Title>
)} )}
{apiError && ( {apiError && (

View File

@ -1,17 +1,18 @@
import React from "react"; import React from "react";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Elements } from "@stripe/react-stripe-js"; import { Elements } from "@stripe/react-stripe-js";
import { Stripe, loadStripe } from "@stripe/stripe-js"; import { Stripe, loadStripe } from "@stripe/stripe-js";
import './discount-screen.css'; import "./discount-screen.css";
import routes from '@/routes'; import routes from "@/routes";
import { useApi } from "@/api"; import { useApi } from "@/api";
import { useAuth } from "@/auth"; import { useAuth } from "@/auth";
import HeaderLogo from '@/components/palmistry/header-logo/header-logo'; import HeaderLogo from "@/components/palmistry/header-logo/header-logo";
import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm"; import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm";
import { ResponseGet } from "@/api/resources/SinglePayment";
const currentProductKey = "skip.trial.subscription.aura"; const currentProductKey = "skip.trial.subscription.aura";
const returnUrl = `${window.location.host}/palmistry/premium-bundle`; const returnUrl = `${window.location.host}/palmistry/premium-bundle`;
@ -23,10 +24,11 @@ export default function DiscountScreen() {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const locale = i18n.language; const locale = i18n.language;
const [price, setPrice] = React.useState(''); const [price, setPrice] = React.useState("");
const [isSuccess] = React.useState(false); const [isSuccess] = React.useState(false);
const [stripePromise, setStripePromise] = React.useState<Promise<Stripe | null> | null>(null); const [stripePromise, setStripePromise] =
const [productId, setProductId] = React.useState(''); React.useState<Promise<Stripe | null> | null>(null);
const [product, setProduct] = React.useState<ResponseGet>();
const [clientSecret, setClientSecret] = React.useState<string | null>(null); const [clientSecret, setClientSecret] = React.useState<string | null>(null);
const [stripePublicKey, setStripePublicKey] = React.useState<string>(""); const [stripePublicKey, setStripePublicKey] = React.useState<string>("");
@ -43,18 +45,22 @@ export default function DiscountScreen() {
setPrice((plan?.price_cents / 100).toFixed(2)); setPrice((plan?.price_cents / 100).toFixed(2));
})(); })();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
React.useEffect(() => { React.useEffect(() => {
(async () => { (async () => {
const products = await api.getSinglePaymentProducts({ token }); const products = await api.getSinglePaymentProducts({ token });
const product = products.find((product) => product.key === currentProductKey); const product = products.find(
(product) => product.key === currentProductKey
);
if (product) { if (product) {
setProductId(product.productId); setProduct(product);
} }
})(); })();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
React.useEffect(() => { React.useEffect(() => {
@ -64,7 +70,13 @@ export default function DiscountScreen() {
}, [stripePublicKey]); }, [stripePublicKey]);
const buy = async () => { const buy = async () => {
if (!user?.id) return; if (!user?.id || !product) return;
const { productId, key } = product;
const paymentInfo = {
productId,
key,
};
const response = await api.createSinglePayment({ const response = await api.createSinglePayment({
token: token, token: token,
@ -80,16 +92,18 @@ export default function DiscountScreen() {
sign: "", sign: "",
age: 0, age: 0,
}, },
paymentInfo: { paymentInfo,
productId,
},
return_url: returnUrl, return_url: returnUrl,
}, },
}); });
if ('paymentIntent' in response && response.paymentIntent.status === "paid" || 'payment' in response && response.payment.status === "paid") { if (
("paymentIntent" in response &&
response.paymentIntent.status === "paid") ||
("payment" in response && response.payment.status === "paid")
) {
goPremiumBundle(); goPremiumBundle();
} else if ('paymentIntent' in response) { } else if ("paymentIntent" in response) {
setClientSecret(response.paymentIntent.data.client_secret); setClientSecret(response.paymentIntent.data.client_secret);
setStripePublicKey(response.paymentIntent.data.public_key); setStripePublicKey(response.paymentIntent.data.public_key);
} }
@ -108,10 +122,14 @@ export default function DiscountScreen() {
<div className="discount-screen__blocks"> <div className="discount-screen__blocks">
<section className="discount-screen__block"> <section className="discount-screen__block">
<span className="discount-screen__price-block">19 for <br /> 1-week plan</span> <span className="discount-screen__price-block">
19 for <br /> 1-week plan
</span>
<div className="discount-screen__details"> <div className="discount-screen__details">
<span className="discount-screen__details-name">Total savings</span> <span className="discount-screen__details-name">
Total savings
</span>
<span className="discount-screen__details-value">0</span> <span className="discount-screen__details-value">0</span>
</div> </div>
@ -120,7 +138,11 @@ export default function DiscountScreen() {
<span className="discount-screen__details-value">yes</span> <span className="discount-screen__details-value">yes</span>
</div> </div>
<button className="discount-screen__button" style={{ minHeight: '38px' }} onClick={goPremiumBundle}> <button
className="discount-screen__button"
style={{ minHeight: "38px" }}
onClick={goPremiumBundle}
>
Start trial Start trial
</button> </button>
</section> </section>
@ -128,10 +150,14 @@ export default function DiscountScreen() {
<section className="discount-screen__block"> <section className="discount-screen__block">
<div className="discount-screen__header-block">save 33%</div> <div className="discount-screen__header-block">save 33%</div>
<span className="discount-screen__price-block">{price} for <br /> 1-week plan</span> <span className="discount-screen__price-block">
{price} for <br /> 1-week plan
</span>
<div className="discount-screen__details"> <div className="discount-screen__details">
<span className="discount-screen__details-name">Total savings</span> <span className="discount-screen__details-name">
Total savings
</span>
<span className="discount-screen__details-value">6.27</span> <span className="discount-screen__details-value">6.27</span>
</div> </div>
@ -148,7 +174,11 @@ export default function DiscountScreen() {
</div> </div>
{stripePromise && clientSecret && ( {stripePromise && clientSecret && (
<div className={`discount-screen__widget${isSuccess ? " discount-screen__widget_success" : ""}`}> <div
className={`discount-screen__widget${
isSuccess ? " discount-screen__widget_success" : ""
}`}
>
<Elements stripe={stripePromise} options={{ clientSecret }}> <Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm returnUrl={returnUrl} /> <CheckoutForm returnUrl={returnUrl} />
</Elements> </Elements>
@ -168,7 +198,9 @@ export default function DiscountScreen() {
/> />
</svg> </svg>
<div className="discount-screen__success-text">Payment success</div> <div className="discount-screen__success-text">
Payment success
</div>
</div> </div>
)} )}
</div> </div>

View File

@ -72,6 +72,7 @@
font-size: 24px; font-size: 24px;
font-weight: 600; font-weight: 600;
line-height: 36px; line-height: 36px;
min-height: 24px;
} }
.palmistry-container__bottom-content { .palmistry-container__bottom-content {
@ -206,7 +207,8 @@
text-align: center; text-align: center;
} }
.palmistry-container__correct-title, .palmistry-container__wrong-title { .palmistry-container__correct-title,
.palmistry-container__wrong-title {
font-size: 18px; font-size: 18px;
font-weight: 700; font-weight: 700;
line-height: 24px; line-height: 24px;
@ -359,13 +361,12 @@
margin-bottom: 34px; margin-bottom: 34px;
text-align: center; text-align: center;
animation: title-opacity 1.5s cubic-bezier(0.37, 0, 0.63, 1); animation: title-opacity 1.5s cubic-bezier(0.37, 0, 0.63, 1);
animation-delay: 13s;
animation-fill-mode: forwards; animation-fill-mode: forwards;
} }
.palmistry-container_type_scan-photo .palmistry-container__waiting-title { .palmistry-container_type_scan-photo .palmistry-container__waiting-title {
animation: waiting-title 0.5s cubic-bezier(0.37, 0, 0.63, 1); animation: waiting-title 0.5s cubic-bezier(0.37, 0, 0.63, 1);
animation-delay: 14.5s; /* animation-delay: 14.5s; */
animation-fill-mode: forwards; animation-fill-mode: forwards;
font-size: 18px; font-size: 18px;
font-weight: 500; font-weight: 500;
@ -377,7 +378,7 @@
.palmistry-container_type_scan-photo .palmistry-container__waiting-description { .palmistry-container_type_scan-photo .palmistry-container__waiting-description {
animation: waiting-description 8s cubic-bezier(0.37, 0, 0.63, 1); animation: waiting-description 8s cubic-bezier(0.37, 0, 0.63, 1);
animation-delay: 15s; /* animation-delay: 15s; */
animation-fill-mode: forwards; animation-fill-mode: forwards;
font-size: 18px; font-size: 18px;
font-weight: 400; font-weight: 400;
@ -428,7 +429,10 @@
width: 100%; width: 100%;
} }
.palmistry-container_type_email .palmistry-container__input input:not(:placeholder-shown) + .input__placeholder { .palmistry-container_type_email
.palmistry-container__input
input:not(:placeholder-shown)
+ .input__placeholder {
font-size: 12px; font-size: 12px;
top: 12px; top: 12px;
width: auto; width: auto;
@ -519,16 +523,19 @@
line-height: 20px; line-height: 20px;
} }
.palmistry-container_type_subscription-plan .palmistry-container__plan:not(:last-child) { .palmistry-container_type_subscription-plan
.palmistry-container__plan:not(:last-child) {
margin-right: 12px; margin-right: 12px;
} }
.palmistry-container_type_subscription-plan .palmistry-container__plan:last-child { .palmistry-container_type_subscription-plan
.palmistry-container__plan:last-child {
position: relative; position: relative;
} }
.palmistry-container_type_subscription-plan .palmistry-container__plan:last-child::after { .palmistry-container_type_subscription-plan
content: ''; .palmistry-container__plan:last-child::after {
content: "";
background: var(--pale-gray); background: var(--pale-gray);
bottom: -24px; bottom: -24px;
height: 15px; height: 15px;
@ -542,11 +549,13 @@
color: var(--black-color-text); color: var(--black-color-text);
} }
.palmistry-container_type_subscription-plan .palmistry-container__plan_active::after { .palmistry-container_type_subscription-plan
.palmistry-container__plan_active::after {
background: var(--strong-blue) !important; background: var(--strong-blue) !important;
} }
.palmistry-container_type_subscription-plan .palmistry-container__subscription-text { .palmistry-container_type_subscription-plan
.palmistry-container__subscription-text {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
line-height: 120%; line-height: 120%;
@ -554,7 +563,8 @@
text-align: center; text-align: center;
} }
.palmistry-container_type_subscription-plan .palmistry-container__subscription-text_active { .palmistry-container_type_subscription-plan
.palmistry-container__subscription-text_active {
color: var(--blue-color-text); color: var(--blue-color-text);
} }

View File

@ -1,16 +1,17 @@
import React from "react"; import React from "react";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from "react-router-dom";
import { Elements } from "@stripe/react-stripe-js"; import { Elements } from "@stripe/react-stripe-js";
import { Stripe, loadStripe } from "@stripe/stripe-js"; import { Stripe, loadStripe } from "@stripe/stripe-js";
import './premium-bundle-screen.css'; import "./premium-bundle-screen.css";
import routes from '@/routes'; import routes from "@/routes";
import HeaderLogo from '@/components/palmistry/header-logo/header-logo'; import HeaderLogo from "@/components/palmistry/header-logo/header-logo";
import { useApi } from '@/api'; import { useApi } from "@/api";
import { useAuth } from "@/auth"; import { useAuth } from "@/auth";
import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm"; import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm";
import { ResponseGet } from "@/api/resources/SinglePayment";
const currentProductKey = "premium.bundle.aura"; const currentProductKey = "premium.bundle.aura";
const returnUrl = window.location.host; const returnUrl = window.location.host;
@ -20,8 +21,9 @@ export default function PremiumBundleScreen() {
const { token, user } = useAuth(); const { token, user } = useAuth();
const api = useApi(); const api = useApi();
const [stripePromise, setStripePromise] = React.useState<Promise<Stripe | null> | null>(null); const [stripePromise, setStripePromise] =
const [productId, setProductId] = React.useState(''); React.useState<Promise<Stripe | null> | null>(null);
const [product, setProduct] = React.useState<ResponseGet>();
const [isSuccess] = React.useState(false); const [isSuccess] = React.useState(false);
const [clientSecret, setClientSecret] = React.useState<string | null>(null); const [clientSecret, setClientSecret] = React.useState<string | null>(null);
const [stripePublicKey, setStripePublicKey] = React.useState<string>(""); const [stripePublicKey, setStripePublicKey] = React.useState<string>("");
@ -30,12 +32,15 @@ export default function PremiumBundleScreen() {
(async () => { (async () => {
const products = await api.getSinglePaymentProducts({ token }); const products = await api.getSinglePaymentProducts({ token });
const product = products.find((product) => product.key === currentProductKey); const product = products.find(
(product) => product.key === currentProductKey
);
if (product) { if (product) {
setProductId(product.productId); setProduct(product);
} }
})(); })();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
React.useEffect(() => { React.useEffect(() => {
@ -45,7 +50,9 @@ export default function PremiumBundleScreen() {
}, [stripePublicKey]); }, [stripePublicKey]);
const buy = async () => { const buy = async () => {
if (!user?.id) return; if (!user?.id || !product) return;
const { productId, key } = product;
const response = await api.createSinglePayment({ const response = await api.createSinglePayment({
token: token, token: token,
@ -63,14 +70,19 @@ export default function PremiumBundleScreen() {
}, },
paymentInfo: { paymentInfo: {
productId, productId,
key,
}, },
return_url: returnUrl, return_url: returnUrl,
}, },
}); });
if ('paymentIntent' in response && response.paymentIntent.status === "paid" || 'payment' in response && response.payment.status === "paid") { if (
("paymentIntent" in response &&
response.paymentIntent.status === "paid") ||
("payment" in response && response.payment.status === "paid")
) {
goHome(); goHome();
} else if ('paymentIntent' in response) { } else if ("paymentIntent" in response) {
setClientSecret(response.paymentIntent.data.client_secret); setClientSecret(response.paymentIntent.data.client_secret);
setStripePublicKey(response.paymentIntent.data.public_key); setStripePublicKey(response.paymentIntent.data.public_key);
} }
@ -87,9 +99,13 @@ export default function PremiumBundleScreen() {
</div> </div>
<div className="premium-bundle-screen__content"> <div className="premium-bundle-screen__content">
<button className="premium-bundle-screen__button-skip" onClick={goHome}>Skip &gt;</button> <button className="premium-bundle-screen__button-skip" onClick={goHome}>
Skip &gt;
</button>
<span className="premium-bundle-screen__title">Get extra insights with our Premium Bundle</span> <span className="premium-bundle-screen__title">
Get extra insights with our Premium Bundle
</span>
<span className="premium-bundle-screen__subtitle"> <span className="premium-bundle-screen__subtitle">
Exclusive offer: recommended for get more insights about what future Exclusive offer: recommended for get more insights about what future
@ -97,7 +113,9 @@ export default function PremiumBundleScreen() {
</span> </span>
<div className="premium-bundle-screen__block-description"> <div className="premium-bundle-screen__block-description">
<span className="premium-bundle-screen__list-title">What your Premium Bundle will include:</span> <span className="premium-bundle-screen__list-title">
What your Premium Bundle will include:
</span>
<div className="premium-bundle-screen__item"> <div className="premium-bundle-screen__item">
<div className="premium-bundle-screen__icon"> <div className="premium-bundle-screen__icon">
@ -180,7 +198,9 @@ export default function PremiumBundleScreen() {
</div> </div>
<div className="premium-bundle-screen__subsection"> <div className="premium-bundle-screen__subsection">
<span className="premium-bundle-screen__one-time-price">One-time price of 19!</span> <span className="premium-bundle-screen__one-time-price">
One-time price of 19!
</span>
<p>Original price is 45. Save 58%!</p> <p>Original price is 45. Save 58%!</p>
</div> </div>
@ -202,7 +222,7 @@ export default function PremiumBundleScreen() {
viewBox="0 0 13 16" viewBox="0 0 13 16"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path d="M11.5556 6.24219H1.44444C0.6467 6.24219 0 6.97481 0 7.87855V13.6058C0 14.5096 0.6467 15.2422 1.44444 15.2422H11.5556C12.3533 15.2422 13 14.5096 13 13.6058V7.87855C13 6.97481 12.3533 6.24219 11.5556 6.24219Z"/> <path d="M11.5556 6.24219H1.44444C0.6467 6.24219 0 6.97481 0 7.87855V13.6058C0 14.5096 0.6467 15.2422 1.44444 15.2422H11.5556C12.3533 15.2422 13 14.5096 13 13.6058V7.87855C13 6.97481 12.3533 6.24219 11.5556 6.24219Z" />
<path <path
fillRule="evenodd" fillRule="evenodd"
clipRule="evenodd" clipRule="evenodd"
@ -214,7 +234,11 @@ export default function PremiumBundleScreen() {
</div> </div>
{stripePromise && clientSecret && ( {stripePromise && clientSecret && (
<div className={`discount-screen__widget${isSuccess ? " discount-screen__widget_success" : ""}`}> <div
className={`discount-screen__widget${
isSuccess ? " discount-screen__widget_success" : ""
}`}
>
<Elements stripe={stripePromise} options={{ clientSecret }}> <Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm returnUrl={returnUrl} /> <CheckoutForm returnUrl={returnUrl} />
</Elements> </Elements>
@ -234,7 +258,9 @@ export default function PremiumBundleScreen() {
/> />
</svg> </svg>
<div className="discount-screen__success-text">Payment success</div> <div className="discount-screen__success-text">
Payment success
</div>
</div> </div>
)} )}
</div> </div>

View File

@ -1,35 +1,38 @@
.scanned-photo { .scanned-photo {
display: flex; display: flex;
height: 477px; /* height: 477px; */
justify-content: center; justify-content: center;
position: relative; position: relative;
width: 291px; width: 100%;
align-items: center;
} }
.scanned-photo__container { .scanned-photo__container {
height: 477px; /* background-color: red; */
height: 100%;
position: relative; position: relative;
width: 291px; width: 100%;
z-index: 2; z-index: 2;
} }
.scanned-photo__stick { .scanned-photo__stick {
animation: scanned-photo-stick-move 5.5s cubic-bezier(0.37, 0, 0.63, 1); animation: scanned-photo-stick-move 5.5s cubic-bezier(0.37, 0, 0.63, 1);
animation-delay: 14.5s; /* animation-delay: 14.5s; */
animation-fill-mode: forwards; animation-fill-mode: forwards;
animation-iteration-count: infinite; animation-iteration-count: infinite;
background-color: var(--strong-blue-text); background-color: var(--strong-blue-text);
height: 2px; height: 2px;
left: -2px; /* left: -2px; */
opacity: 0; opacity: 0;
position: absolute; position: absolute;
width: 96px; width: 100%;
z-index: 5; z-index: 5;
} }
.scanned-photo__image { .scanned-photo__image {
height: 100%; /* height: 100%; */
object-fit: cover; /* object-fit: cover; */
object-fit: contain;
width: 100%; width: 100%;
} }
@ -71,7 +74,7 @@
.scanned-photo__line { .scanned-photo__line {
stroke-linecap: round; stroke-linecap: round;
stroke-linejoin: round; stroke-linejoin: round;
stroke-width: 3px; stroke-width: 2px;
fill-rule: evenodd; fill-rule: evenodd;
clip-rule: evenodd; clip-rule: evenodd;
stroke-miterlimit: 1.5; stroke-miterlimit: 1.5;
@ -84,7 +87,7 @@
.scanned-photo__line_heart { .scanned-photo__line_heart {
stroke: #f8d90f; stroke: #f8d90f;
animation-delay: 4.5s; /* animation-delay: 4.5s; */
} }
.scanned-photo__line_life { .scanned-photo__line_life {
@ -93,12 +96,12 @@
.scanned-photo__line_head { .scanned-photo__line_head {
stroke: #00d114; stroke: #00d114;
animation-delay: 1.5s; /* animation-delay: 1.5s; */
} }
.scanned-photo__line_fate { .scanned-photo__line_fate {
stroke: #05ced8; stroke: #05ced8;
animation-delay: 3s; /* animation-delay: 3s; */
} }
.scanned-photo__decoration { .scanned-photo__decoration {
@ -107,19 +110,35 @@
justify-content: center; justify-content: center;
opacity: 0; opacity: 0;
animation: scanned-photo-opacity 1.5s cubic-bezier(0.37, 0, 0.63, 1); animation: scanned-photo-opacity 1.5s cubic-bezier(0.37, 0, 0.63, 1);
animation-delay: 12s; /* animation-delay: 12s; */
animation-fill-mode: forwards; animation-fill-mode: forwards;
height: 220px; height: 220px;
margin-top: -10px;
position: absolute; position: absolute;
width: 220px; width: 220px;
} }
.scanned-photo__decoration__corners { .scanned-photo__decoration__corners {
animation: scanned-photo-corners-opacity 1.5s cubic-bezier(0.37, 0, 0.63, 1); animation: scanned-photo-corners-opacity 1.5s cubic-bezier(0.37, 0, 0.63, 1);
animation-delay: 13.5s; /* animation-delay: 13.5s; */
animation-fill-mode: forwards; animation-fill-mode: forwards;
background: linear-gradient(to right, var(--strong-blue-text) 2px, transparent 2px) 0 0, linear-gradient(to right, var(--strong-blue-text) 2px, transparent 2px) 0 100%, linear-gradient(to left, var(--strong-blue-text) 2px, transparent 2px) 100% 0, linear-gradient(to left, var(--strong-blue-text) 2px, transparent 2px) 100% 100%, linear-gradient(to bottom, var(--strong-blue-text) 2px, transparent 2px) 0 0, linear-gradient(to bottom, var(--strong-blue-text) 2px, transparent 2px) 100% 0, linear-gradient(to top, var(--strong-blue-text) 2px, transparent 2px) 0 100%, linear-gradient(to top, var(--strong-blue-text) 2px, transparent 2px) 100% 100%; background: linear-gradient(
to right,
var(--strong-blue-text) 2px,
transparent 2px
)
0 0,
linear-gradient(to right, var(--strong-blue-text) 2px, transparent 2px) 0
100%,
linear-gradient(to left, var(--strong-blue-text) 2px, transparent 2px) 100%
0,
linear-gradient(to left, var(--strong-blue-text) 2px, transparent 2px) 100%
100%,
linear-gradient(to bottom, var(--strong-blue-text) 2px, transparent 2px) 0 0,
linear-gradient(to bottom, var(--strong-blue-text) 2px, transparent 2px)
100% 0,
linear-gradient(to top, var(--strong-blue-text) 2px, transparent 2px) 0 100%,
linear-gradient(to top, var(--strong-blue-text) 2px, transparent 2px) 100%
100%;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 15px 15px; background-size: 15px 15px;
height: 100%; height: 100%;
@ -156,68 +175,74 @@
animation-fill-mode: forwards; animation-fill-mode: forwards;
} }
.scanned-photo_small .scanned-photo__image {
height: 100%;
}
@keyframes scanned-photo-resize { @keyframes scanned-photo-resize {
100% { 100% {
height: 207px; height: 207px;
} /* align-items: center; */
}
} }
@keyframes scanned-photo-container-resize { @keyframes scanned-photo-container-resize {
100% { 100% {
border: 3px solid #fff; /* border: 3px solid #fff; */
height: 159px; height: 70.7%;
margin-top: 20px; /* margin-top: 20px; */
width: 97px; width: auto;
} /* aspect-ratio: 1 / 1; */
}
} }
@keyframes scanned-photo-opacity { @keyframes scanned-photo-opacity {
100% { 100% {
opacity: 1; opacity: 1;
} }
} }
@keyframes scanned-photo-corners-opacity { @keyframes scanned-photo-corners-opacity {
10% { 10% {
opacity: 0; opacity: 0;
} }
20% { 20% {
opacity: 0.7; opacity: 0.7;
} }
40% { 40% {
opacity: 0.3; opacity: 0.3;
} }
50% { 50% {
opacity: 0.6; opacity: 0.6;
} }
100% { 100% {
opacity: 1; opacity: 1;
} }
} }
@keyframes scanned-photo-stick-move { @keyframes scanned-photo-stick-move {
0% { 0% {
opacity: 1; opacity: 1;
top: 0; top: 0;
} }
50% { 50% {
opacity: 1; opacity: 1;
top: 100%; top: 100%;
} }
100% { 100% {
opacity: 1; opacity: 1;
top: 0; top: 0;
} }
} }
@keyframes finger-show { @keyframes finger-show {
100% { 100% {
transform: scale(1); transform: scale(1);
} }
} }
@keyframes line-show { @keyframes line-show {
100% { 100% {
stroke-dashoffset: 0; stroke-dashoffset: 0;
} }
} }

View File

@ -1,27 +1,90 @@
import './scanned-photo.css'; import { IPalmistryLine, IPalmistryPoint } from "@/api/resources/Palmistry";
import "./scanned-photo.css";
import { useCallback, useEffect, useRef, useState } from "react";
type Props = { type Props = {
photo: string; photo: string;
small: boolean; small: boolean;
displayLines: boolean; displayLines: boolean;
lines: IPalmistryLine[];
lineChangeDelay: number;
startDelay: number;
}; };
export default function StepScanPhoto(props: Props) { export default function StepScanPhoto(props: Props) {
const className = ['scanned-photo']; const className = ["scanned-photo"];
const { lines, lineChangeDelay } = props;
const imageRef = useRef<HTMLImageElement>(null);
const linesRef = useRef<SVGPathElement[]>([]);
const [isImageLoaded, setIsImageLoaded] = useState(false);
const [imageWidth, setImageWidth] = useState(0);
const [imageHeight, setImageHeight] = useState(0);
if (props.small) { if (props.small) {
className.push('scanned-photo_small'); className.push("scanned-photo_small");
} }
useEffect(() => {
if (isImageLoaded && imageRef.current) {
setImageWidth(imageRef.current.width || 0);
setImageHeight(imageRef.current.height || 0);
}
}, [isImageLoaded]);
const getCoordinatesString = useCallback(
(points: IPalmistryPoint[]) => {
const coordinatesString = `M ${points[0]?.x * imageWidth} ${
points[0]?.y * imageHeight
}`;
return points.reduce(
(acc, point) =>
`${acc} L ${point?.x * imageWidth} ${point?.y * imageHeight}`,
coordinatesString
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[lines, isImageLoaded, imageWidth, imageHeight]
);
const getLineLength = (line: SVGPathElement) => {
return line?.getTotalLength();
};
// const getAnimationDelayOfLine = (index: number) => {
// return `${lineChangeDelay * index + startDelay}ms`;
// };
return ( return (
<div className={className.join(' ')}> <div
className={className.join(" ")}
style={{
height: imageHeight ? `${imageHeight}px` : "auto",
}}
>
<div className="scanned-photo__container"> <div className="scanned-photo__container">
<div className="scanned-photo__stick" /> <div
className="scanned-photo__stick"
style={{
animationDelay: `${lineChangeDelay * lines.length + 2500}ms`,
maxWidth: `${imageWidth}px`,
}}
/>
<img className="scanned-photo__image" alt="PalmIcon" src={props.photo} width={291} height={477}/> <img
className="scanned-photo__image"
<svg viewBox="0 0 291 477" className="scanned-photo__svg-objects"> alt="PalmIcon"
<svg x="235" y="211" height="24px" width="24px"> src={props.photo}
ref={imageRef}
onLoad={() => setIsImageLoaded(true)}
// width={imageWidth}
// height={imageHeight}
/>
{!!imageHeight && !!imageWidth && (
<svg
viewBox={`0 0 ${imageWidth} ${imageHeight}`}
className="scanned-photo__svg-objects"
>
{/* <svg x="235" y="211" height="24px" width="24px">
<circle <circle
cx="50%" cx="50%"
cy="50%" cy="50%"
@ -119,16 +182,33 @@ export default function StepScanPhoto(props: Props) {
strokeWidth="0.3" strokeWidth="0.3"
className="scanned-photo__finger-point scanned-photo__finger-point_little" className="scanned-photo__finger-point scanned-photo__finger-point_little"
/> />
</svg> </svg> */}
{props.displayLines && ( {props.displayLines && (
<> <>
<path {lines.map((line, index) => (
className="scanned-photo__line scanned-photo__line_heart" <path
key={index}
className={`scanned-photo__line scanned-photo__line_${line?.line}`}
d={getCoordinatesString(line?.points)}
ref={(el) =>
(linesRef.current[index] = el as SVGPathElement)
}
style={{
strokeDasharray:
getLineLength(linesRef.current[index]) || 500,
strokeDashoffset:
getLineLength(linesRef.current[index]) || 500,
animationDelay: `${lineChangeDelay * (index + 1)}ms`,
}}
/>
))}
{/* <path
className={`scanned-photo__line scanned-photo__line_heart`}
d="M 95 334 L 99 330 L 104 327 L 109 323 L 113 319 L 118 315 L 123 311 L 128 308 L 132 304 L 137 301 L 142 298 L 146 296 L 151 293 L 156 291 L 160 289 L 165 287 L 170 286 L 174 284 L 179 283 L 184 283 L 189 282 L 193 282 L 198 283 L 203 284 L 207 285" d="M 95 334 L 99 330 L 104 327 L 109 323 L 113 319 L 118 315 L 123 311 L 128 308 L 132 304 L 137 301 L 142 298 L 146 296 L 151 293 L 156 291 L 160 289 L 165 287 L 170 286 L 174 284 L 179 283 L 184 283 L 189 282 L 193 282 L 198 283 L 203 284 L 207 285"
style={{ strokeDasharray: 128.14, strokeDashoffset: 128.14 }} style={{ strokeDasharray: 128.14, strokeDashoffset: 128.14 }}
/> /> */}
<path {/* <path
className="scanned-photo__line scanned-photo__line_life" className="scanned-photo__line scanned-photo__line_life"
d="M 205 283 L 193 291 L 181 299 L 170 306 L 160 314 L 153 322 L 147 329 L 143 337 L 139 345 L 136 352 L 133 360 L 130 368 L 128 376 L 126 383 L 125 391 L 125 399 L 126 406 L 128 414 L 132 422 L 137 429 L 143 437 L 149 445 L 156 452" d="M 205 283 L 193 291 L 181 299 L 170 306 L 160 314 L 153 322 L 147 329 L 143 337 L 139 345 L 136 352 L 133 360 L 130 368 L 128 376 L 126 383 L 125 391 L 125 399 L 126 406 L 128 414 L 132 422 L 137 429 L 143 437 L 149 445 L 156 452"
style={{ strokeDasharray: 211.483, strokeDashoffset: 211.483 }} style={{ strokeDasharray: 211.483, strokeDashoffset: 211.483 }}
@ -142,14 +222,25 @@ export default function StepScanPhoto(props: Props) {
className="scanned-photo__line scanned-photo__line_fate" className="scanned-photo__line scanned-photo__line_fate"
d="M 134 260 L 129 299 L 125 306 L 122 314 L 120 322 L 118 329 L 116 337 L 115 345 L 114 352" d="M 134 260 L 129 299 L 125 306 L 122 314 L 120 322 L 118 329 L 116 337 L 115 345 L 114 352"
style={{ strokeDasharray: 94.8313, strokeDashoffset: 94.8313 }} style={{ strokeDasharray: 94.8313, strokeDashoffset: 94.8313 }}
/> /> */}
</> </>
)} )}
</svg> </svg>
)}
</div> </div>
<div className="scanned-photo__decoration"> <div
<div className="scanned-photo__decoration__corners"> className="scanned-photo__decoration"
style={{
animationDelay: `${lineChangeDelay * lines?.length}ms`,
}}
>
<div
className="scanned-photo__decoration__corners"
style={{
animationDelay: `${lineChangeDelay * lines?.length + 1500}ms`,
}}
>
<div className="scanned-photo__decoration__light-blue-circle" /> <div className="scanned-photo__decoration__light-blue-circle" />
<svg <svg
@ -161,11 +252,25 @@ export default function StepScanPhoto(props: Props) {
viewBox="0 0 220 220" viewBox="0 0 220 220"
enableBackground="new 0 0 0 0" enableBackground="new 0 0 0 0"
> >
<circle fill="none" stroke="#EFF2FD" strokeWidth="2" cx="110" cy="110" r="105"/> <circle
<circle fill="#066fde" stroke="none" strokeWidth="3" cx="110" cy="215" r="4"> fill="none"
stroke="#EFF2FD"
strokeWidth="2"
cx="110"
cy="110"
r="105"
/>
<circle
fill="#066fde"
stroke="none"
strokeWidth="3"
cx="110"
cy="215"
r="4"
>
<animateTransform <animateTransform
attributeName="transform" attributeName="transform"
dur="15s" dur={`${lineChangeDelay * lines?.length + 3000}ms`}
type="rotate" type="rotate"
from="0 110 110" from="0 110 110"
to="360 110 110" to="360 110 110"

View File

@ -1,96 +1,112 @@
import React from 'react'; import { useEffect, useRef, useState } from "react";
import { Step } from '@/hooks/palmistry/use-steps'; import { Step } from "@/hooks/palmistry/use-steps";
import useSteps from '@/hooks/palmistry/use-steps'; import useSteps from "@/hooks/palmistry/use-steps";
import ScannedPhoto from '@/components/palmistry/scanned-photo/scanned-photo'; import ScannedPhoto from "@/components/palmistry/scanned-photo/scanned-photo";
import { useSelector } from "react-redux";
import { selectors } from "@/store";
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
enum PalmElement {
ThumbFinger = 'Thumb finger',
IndexFinger = 'Index finger',
MiddleFinger = 'Middle finger',
RingFinger = 'Ring finger',
LittleFinger = 'Little finger',
LifeLine = 'Life line',
HeadLine = 'Head line',
FateLine = 'Fate line',
HeartLine = 'Heart line',
}
const fingers = [
PalmElement.ThumbFinger,
PalmElement.IndexFinger,
PalmElement.MiddleFinger,
PalmElement.RingFinger,
PalmElement.LittleFinger,
];
const lines = [
PalmElement.LifeLine,
PalmElement.HeadLine,
PalmElement.FateLine,
PalmElement.HeartLine,
];
const palmElements = [
...fingers,
...lines,
];
const fingerChangeDelay = 1000;
const lineChangeDelay = 1500; const lineChangeDelay = 1500;
const goNextDelay = 12000; const startDelay = 500;
// const goNextDelay = 12000;
export default function StepScanPhoto() { export default function StepScanPhoto() {
const steps = useSteps(); const steps = useSteps();
const navigate = useNavigate();
const storedPhoto = steps.getStoredValue(Step.Upload); const storedPhoto = steps.getStoredValue(Step.Upload);
const [curentElementIndex, setCurrentElementIndex] = React.useState(0); const lines = useSelector(selectors.selectPalmistryLines);
const [photo, setPhoto] = React.useState('');
const [smallPhotoState, setSmallPhotoState] = React.useState(false);
const [title, setTitle] = React.useState(palmElements[0]);
const [sholdDisplayPalmLines, setSholdDisplayPalmLines] = React.useState(false);
const prevElementIndex = React.useRef<number | null>(null); const [currentElementIndex, setCurrentElementIndex] = useState(0);
const [smallPhotoState, setSmallPhotoState] = useState(false);
const [title, setTitle] = useState("");
const [shouldDisplayPalmLines, setShouldDisplayPalmLines] = useState(false);
const prevElementIndex = useRef<number | null>(null);
const goNextElement = (delay: number) => { const goNextElement = (delay: number) => {
setTimeout(() => { setTimeout(() => {
setTitle(lines[currentElementIndex]?.line);
setCurrentElementIndex((prevState) => prevState + 1); setCurrentElementIndex((prevState) => prevState + 1);
}, delay); }, delay);
}; };
React.useEffect(() => { useEffect(() => {
if (storedPhoto) { if (!lines.length) {
setPhoto(storedPhoto); return navigate(routes.client.palmistryUpload());
} }
}, [storedPhoto]); }, [lines, navigate]);
React.useEffect(() => { useEffect(() => {
if (curentElementIndex < palmElements.length && curentElementIndex !== prevElementIndex.current) { // if (currentElementIndex === 0) {
prevElementIndex.current = curentElementIndex; // new Promise((resolve) => setTimeout(resolve, startDelay));
setTitle(palmElements[curentElementIndex]); // }
goNextElement(fingers.includes(palmElements[curentElementIndex]) ? fingerChangeDelay : lineChangeDelay); if (
setSholdDisplayPalmLines(lines.includes(palmElements[curentElementIndex])); currentElementIndex < lines?.length &&
currentElementIndex !== prevElementIndex.current
) {
prevElementIndex.current = currentElementIndex;
goNextElement(lineChangeDelay);
setShouldDisplayPalmLines(lines?.includes(lines[currentElementIndex]));
} }
if (curentElementIndex >= palmElements.length) { if (currentElementIndex >= lines?.length) {
setSmallPhotoState(true); setTimeout(() => {
setSmallPhotoState(true);
setTimeout(steps.goNext, goNextDelay); }, lineChangeDelay);
setTimeout(steps.goNext, lineChangeDelay * lines.length + 8000);
} }
}, [curentElementIndex]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentElementIndex]);
if (!photo) return null; // if (!storedPhoto) {
// return <Title variant="h4">Upload your photo</Title>;
// }
// console.log(shouldDisplayPalmLines);
// return <Title variant="h4">Upload your photo</Title>;
return ( return (
<> <>
<h2 className="palmistry-container__title">{title}</h2> <h2
className="palmistry-container__title"
style={{
animationDelay: `${lineChangeDelay * lines.length}ms`,
}}
>
{title}
</h2>
{/* <pre>{JSON.stringify(lines, null, 2)}</pre> */}
<ScannedPhoto
photo={storedPhoto}
small={smallPhotoState}
lineChangeDelay={lineChangeDelay}
startDelay={startDelay}
displayLines={shouldDisplayPalmLines}
lines={lines}
/>
<ScannedPhoto photo={photo} small={smallPhotoState} displayLines={sholdDisplayPalmLines}/> <h2
className="palmistry-container__waiting-title"
style={{
animationDelay: `${lineChangeDelay * lines.length + 2500}ms`,
}}
>
We are putting together a comprehensive Palmistry Reading just for you!
</h2>
<h2 className="palmistry-container__waiting-title">We are putting together a comprehensive Palmistry Reading just for you!</h2> <h3
className="palmistry-container__waiting-description"
<h3 className="palmistry-container__waiting-description">Wow, looks like there is a lot we can tell about your ambitious and strong self-confident future.</h3> style={{
animationDelay: `${lineChangeDelay * lines.length + 3000}ms`,
}}
>
Wow, looks like there is a lot we can tell about your ambitious and
strong self-confident future.
</h3>
</> </>
); );
} }

View File

@ -1,12 +1,15 @@
import React from 'react'; import { useEffect, useState } from "react";
import useSteps from '../../../hooks/palmistry/use-steps'; import useSteps from "../../../hooks/palmistry/use-steps";
import Button from '../button/button'; import Button from "../button/button";
import BiometricData from '../biometric-data/biometric-data'; import BiometricData from "../biometric-data/biometric-data";
import UploadModal from '../upload-modal/upload-modal'; import UploadModal from "../upload-modal/upload-modal";
import ModalOverlay from '../modal-overlay/modal-overlay'; import ModalOverlay from "../modal-overlay/modal-overlay";
import PalmRecognitionErrorModal from '../palm-recognition-error-modal/palm-recognition-error-modal'; import PalmRecognitionErrorModal from "../palm-recognition-error-modal/palm-recognition-error-modal";
import PalmCameraModal from '../palm-camera-modal/palm-camera-modal'; import PalmCameraModal from "../palm-camera-modal/palm-camera-modal";
import { useApi } from "@/api";
import { useDispatch } from "react-redux";
import { actions } from "@/store";
type Props = { type Props = {
onOpenModal: (isOpen: boolean) => void; onOpenModal: (isOpen: boolean) => void;
@ -14,12 +17,15 @@ type Props = {
export default function StepUpload(props: Props) { export default function StepUpload(props: Props) {
const steps = useSteps(); const steps = useSteps();
const api = useApi();
const dispatch = useDispatch();
const [uploadMenuModalIsOpen, setUploadMenuModalIsOpen] = React.useState(false); const [uploadMenuModalIsOpen, setUploadMenuModalIsOpen] = useState(false);
const [isUpladProcessing, setIsUpladProcessing] = React.useState(false); const [isUpladProcessing, setIsUpladProcessing] = useState(false);
const [recognitionErrorModalIsOpen, setRecognitionErrorModalIsOpen] = React.useState(false); const [recognitionErrorModalIsOpen, setRecognitionErrorModalIsOpen] =
const [palmCameraModalIsOpen, setPalmCameraModalIsOpen] = React.useState(false); useState(false);
const [palmPhoto, setPalmPhoto] = React.useState<string>(); const [palmCameraModalIsOpen, setPalmCameraModalIsOpen] = useState(false);
const [palmPhoto, setPalmPhoto] = useState<string>();
// const imitateRequestError = () => { // const imitateRequestError = () => {
// setTimeout(() => { // setTimeout(() => {
@ -28,11 +34,21 @@ export default function StepUpload(props: Props) {
// }, 2000); // }, 2000);
// }; // };
const onSelectFile = (event: React.ChangeEvent<HTMLInputElement>) => { const getLines = async (file: File | Blob) => {
const formData = new FormData();
formData.append("file", file);
const result = await api.getPalmistryLines({ formData });
dispatch(actions.palmistry.update({ lines: result }));
};
const onSelectFile = async (event: React.ChangeEvent<HTMLInputElement>) => {
setUploadMenuModalIsOpen(false); setUploadMenuModalIsOpen(false);
if (!event.target.files || event.target.files.length === 0) return; if (!event.target.files || event.target.files.length === 0) return;
await getLines(event.target.files[0]);
setIsUpladProcessing(true); setIsUpladProcessing(true);
const reader = new FileReader(); const reader = new FileReader();
@ -44,46 +60,65 @@ export default function StepUpload(props: Props) {
reader.readAsDataURL(event.target.files[0]); reader.readAsDataURL(event.target.files[0]);
}; };
const onTakePhoto = (photo: string) => { const DataURIToBlob = (dataURI: string) => {
const splitDataURI = dataURI.split(",");
const byteString =
splitDataURI[0].indexOf("base64") >= 0
? atob(splitDataURI[1])
: decodeURI(splitDataURI[1]);
const mimeString = splitDataURI[0].split(":")[1].split(";")[0];
const ia = new Uint8Array(byteString.length);
for (let i = 0; i < byteString.length; i++)
ia[i] = byteString.charCodeAt(i);
return new Blob([ia], { type: mimeString });
};
const onTakePhoto = async (photo: string) => {
const file = DataURIToBlob(photo);
await getLines(file);
setPalmPhoto(photo as string); setPalmPhoto(photo as string);
setUploadMenuModalIsOpen(false); setUploadMenuModalIsOpen(false);
setPalmCameraModalIsOpen(false); setPalmCameraModalIsOpen(false);
}; };
React.useEffect(() => { useEffect(() => {
if (palmPhoto) { if (palmPhoto) {
fetch(
'https://palmistry.hint.app/api/processing',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: palmPhoto }),
},
);
setIsUpladProcessing(false); setIsUpladProcessing(false);
steps.saveCurrent(palmPhoto); steps.saveCurrent(palmPhoto);
steps.goNext(); steps.goNext();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [palmPhoto]); }, [palmPhoto]);
React.useEffect(() => { useEffect(() => {
if (recognitionErrorModalIsOpen || palmCameraModalIsOpen) { if (recognitionErrorModalIsOpen || palmCameraModalIsOpen) {
props.onOpenModal(true); props.onOpenModal(true);
} else { } else {
props.onOpenModal(false); props.onOpenModal(false);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [recognitionErrorModalIsOpen, palmCameraModalIsOpen]); }, [recognitionErrorModalIsOpen, palmCameraModalIsOpen]);
return ( return (
<> <>
<h2 className="palmistry-container__title">Take your palm picture as instructed</h2> <h2 className="palmistry-container__title">
Take your palm picture as instructed
</h2>
<div className="palmistry-container__image-wrapper"> <div className="palmistry-container__image-wrapper">
<div className="palmistry-container__image-wrapper-container"> <div className="palmistry-container__image-wrapper-container">
<p className="palmistry-container__correct-title">Correct</p> <p className="palmistry-container__correct-title">Correct</p>
<div className="palmistry-container__wrapper-correct-palm-image"> <div className="palmistry-container__wrapper-correct-palm-image">
<svg width="106" height="193" viewBox="0 0 106 193" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg
width="106"
height="193"
viewBox="0 0 106 193"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M13.9463 117.011C14.954 138.058 32.1776 154.86 53.2104 154.86C74.2432 154.86 91.4669 138.051 92.4745 117.011H13.9463Z" d="M13.9463 117.011C14.954 138.058 32.1776 154.86 53.2104 154.86C74.2432 154.86 91.4669 138.051 92.4745 117.011H13.9463Z"
fill="#FFFFFF" fill="#FFFFFF"
@ -155,12 +190,23 @@ export default function StepUpload(props: Props) {
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
></path> ></path>
<rect x="3" y="3" width="100" height="187" rx="6" stroke="#9BACD7" strokeWidth="5"></rect> <rect
x="3"
y="3"
width="100"
height="187"
rx="6"
stroke="#9BACD7"
strokeWidth="5"
></rect>
<path <path
d="M3 169H103V184C103 187.314 100.314 190 97 190H9C5.68629 190 3 187.314 3 184V169Z" d="M3 169H103V184C103 187.314 100.314 190 97 190H9C5.68629 190 3 187.314 3 184V169Z"
fill="#9BACD7" fill="#9BACD7"
></path> ></path>
<path d="M3 9C3 5.68629 5.68629 3 9 3H97C100.314 3 103 5.68629 103 9V14H3V9Z" fill="#9BACD7"></path> <path
d="M3 9C3 5.68629 5.68629 3 9 3H97C100.314 3 103 5.68629 103 9V14H3V9Z"
fill="#9BACD7"
></path>
<circle cx="54" cy="181" r="6" fill="#FFFFFF"></circle> <circle cx="54" cy="181" r="6" fill="#FFFFFF"></circle>
</svg> </svg>
@ -184,7 +230,12 @@ export default function StepUpload(props: Props) {
<p className="palmistry-container__wrong-title">Wrong</p> <p className="palmistry-container__wrong-title">Wrong</p>
<div className="palmistry-container__uncorrect-images"> <div className="palmistry-container__uncorrect-images">
<svg width="59" height="87" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg
width="59"
height="87"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M8.146 54.95c.49 10.243 8.874 18.421 19.111 18.421 10.238 0 18.62-8.181 19.111-18.422H8.146z" d="M8.146 54.95c.49 10.243 8.874 18.421 19.111 18.421 10.238 0 18.62-8.181 19.111-18.422H8.146z"
fill="#FFFFFF" fill="#FFFFFF"
@ -256,19 +307,52 @@ export default function StepUpload(props: Props) {
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
></path> ></path>
<rect x="3.073" y="2.998" width="48.181" height="81" rx="6" stroke="#9BACD7" strokeWidth="5"></rect> <rect
x="3.073"
y="2.998"
width="48.181"
height="81"
rx="6"
stroke="#9BACD7"
strokeWidth="5"
></rect>
<path <path
d="M3.073 75.27h48.181v2.728a6 6 0 0 1-6 6H9.074a6 6 0 0 1-6-6v-2.729zM3.073 7.537a4.539 4.539 0 0 1 4.539-4.539h39.104a4.539 4.539 0 0 1 4.538 4.539H3.074z" d="M3.073 75.27h48.181v2.728a6 6 0 0 1-6 6H9.074a6 6 0 0 1-6-6v-2.729zM3.073 7.537a4.539 4.539 0 0 1 4.539-4.539h39.104a4.539 4.539 0 0 1 4.538 4.539H3.074z"
fill="#9BACD7" fill="#9BACD7"
></path> ></path>
<circle cx="26.99" cy="80.159" r="2.444" fill="#FFFFFF"></circle> <circle cx="26.99" cy="80.159" r="2.444" fill="#FFFFFF"></circle>
<path fill="#FF5758" d="m39.053 58.875 3.326-3.61L58.62 70.234l-3.326 3.61z"></path> <path
<path fill="#FF5758" d="m54.91 55.01 3.471 3.47-15.617 15.618-3.471-3.47z"></path> fill="#FF5758"
d="m39.053 58.875 3.326-3.61L58.62 70.234l-3.326 3.61z"
></path>
<path
fill="#FF5758"
d="m54.91 55.01 3.471 3.47-15.617 15.618-3.471-3.47z"
></path>
</svg> </svg>
<svg width="58" height="87" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg
<mask id="a" maskUnits="userSpaceOnUse" x="2" y="2" width="49" height="82"> width="58"
<rect x="2.621" y="2.998" width="48.181" height="81" rx="6" fill="#D9D9D9"></rect> height="87"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<mask
id="a"
maskUnits="userSpaceOnUse"
x="2"
y="2"
width="49"
height="82"
>
<rect
x="2.621"
y="2.998"
width="48.181"
height="81"
rx="6"
fill="#D9D9D9"
></rect>
</mask> </mask>
<g mask="url(#a)"> <g mask="url(#a)">
<path <path
@ -343,17 +427,36 @@ export default function StepUpload(props: Props) {
strokeLinejoin="round" strokeLinejoin="round"
></path> ></path>
</g> </g>
<rect x="2.621" y="2.998" width="48.181" height="81" rx="6" stroke="#9BACD7" strokeWidth="5"></rect> <rect
x="2.621"
y="2.998"
width="48.181"
height="81"
rx="6"
stroke="#9BACD7"
strokeWidth="5"
></rect>
<path <path
d="M2.621 75.27h48.181v2.728a6 6 0 0 1-6 6H8.622a6 6 0 0 1-6-6v-2.729zM2.621 7.537A4.539 4.539 0 0 1 7.16 2.998h39.103a4.539 4.539 0 0 1 4.54 4.539H2.62z" d="M2.621 75.27h48.181v2.728a6 6 0 0 1-6 6H8.622a6 6 0 0 1-6-6v-2.729zM2.621 7.537A4.539 4.539 0 0 1 7.16 2.998h39.103a4.539 4.539 0 0 1 4.54 4.539H2.62z"
fill="#9BACD7" fill="#9BACD7"
></path> ></path>
<circle cx="26.538" cy="80.159" r="2.444" fill="#FFFFFF"></circle> <circle cx="26.538" cy="80.159" r="2.444" fill="#FFFFFF"></circle>
<path fill="#FF5758" d="m38.148 58.02 3.326-3.61 16.243 14.968-3.326 3.61z"></path> <path
<path fill="#FF5758" d="m54.007 54.154 3.47 3.47L41.86 73.244l-3.47-3.47z"></path> fill="#FF5758"
d="m38.148 58.02 3.326-3.61 16.243 14.968-3.326 3.61z"
></path>
<path
fill="#FF5758"
d="m54.007 54.154 3.47 3.47L41.86 73.244l-3.47-3.47z"
></path>
</svg> </svg>
<svg width="62" height="87" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg
width="62"
height="87"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M45.491 28.62c-.49-10.245-8.873-18.423-19.11-18.423-10.238 0-18.621 8.181-19.111 18.422H45.49z" d="M45.491 28.62c-.49-10.245-8.873-18.423-19.11-18.423-10.238 0-18.621 8.181-19.111 18.422H45.49z"
fill="#FFFFFF" fill="#FFFFFF"
@ -425,17 +528,36 @@ export default function StepUpload(props: Props) {
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
></path> ></path>
<rect x="2.717" y="2.998" width="48.181" height="81" rx="6" stroke="#9BACD7" strokeWidth="5"></rect> <rect
x="2.717"
y="2.998"
width="48.181"
height="81"
rx="6"
stroke="#9BACD7"
strokeWidth="5"
></rect>
<path <path
d="M2.717 75.27h48.18v2.728a6 6 0 0 1-6 6H8.718a6 6 0 0 1-6-6v-2.729zM2.717 7.537a4.539 4.539 0 0 1 4.539-4.539h39.103a4.539 4.539 0 0 1 4.539 4.539H2.717z" d="M2.717 75.27h48.18v2.728a6 6 0 0 1-6 6H8.718a6 6 0 0 1-6-6v-2.729zM2.717 7.537a4.539 4.539 0 0 1 4.539-4.539h39.103a4.539 4.539 0 0 1 4.539 4.539H2.717z"
fill="#9BACD7" fill="#9BACD7"
></path> ></path>
<circle cx="26.633" cy="80.159" r="2.444" fill="#FFFFFF"></circle> <circle cx="26.633" cy="80.159" r="2.444" fill="#FFFFFF"></circle>
<path fill="#FF5758" d="m41.645 58.875 3.326-3.61 16.242 14.968-3.326 3.61z"></path> <path
<path fill="#FF5758" d="m57.502 55.01 3.47 3.47-15.617 15.618-3.47-3.47z"></path> fill="#FF5758"
d="m41.645 58.875 3.326-3.61 16.242 14.968-3.326 3.61z"
></path>
<path
fill="#FF5758"
d="m57.502 55.01 3.47 3.47-15.617 15.618-3.47-3.47z"
></path>
</svg> </svg>
<svg width="67" height="111" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg
width="67"
height="111"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M.424 79.086c.85 17.745 15.372 31.911 33.105 31.911 17.733 0 32.255-14.172 33.104-31.911H.424z" d="M.424 79.086c.85 17.745 15.372 31.911 33.105 31.911 17.733 0 32.255-14.172 33.104-31.911H.424z"
fill="#FFFFFF" fill="#FFFFFF"
@ -452,14 +574,28 @@ export default function StepUpload(props: Props) {
d="M33.545 107.92c-16.064 0-29.333-12.823-30.188-28.858H.332C1.182 96.808 15.812 111 33.545 111s32.532-14.197 33.382-31.936h-3.043c-.837 16.052-14.275 28.857-30.339 28.857z" d="M33.545 107.92c-16.064 0-29.333-12.823-30.188-28.858H.332C1.182 96.808 15.812 111 33.545 111s32.532-14.197 33.382-31.936h-3.043c-.837 16.052-14.275 28.857-30.339 28.857z"
fill="#DEE5F8" fill="#DEE5F8"
></path> ></path>
<rect x="8.207" y="14.375" width="48.755" height="81.964" rx="6" stroke="#9BACD7" strokeWidth="5"></rect> <rect
x="8.207"
y="14.375"
width="48.755"
height="81.964"
rx="6"
stroke="#9BACD7"
strokeWidth="5"
></rect>
<path <path
d="M8.207 87.508h48.755v2.832a6 6 0 0 1-6 6H14.207a6 6 0 0 1-6-6v-2.832zM8.207 18.968a4.593 4.593 0 0 1 4.593-4.593h39.569a4.593 4.593 0 0 1 4.593 4.593H8.207z" d="M8.207 87.508h48.755v2.832a6 6 0 0 1-6 6H14.207a6 6 0 0 1-6-6v-2.832zM8.207 18.968a4.593 4.593 0 0 1 4.593-4.593h39.569a4.593 4.593 0 0 1 4.593 4.593H8.207z"
fill="#9BACD7" fill="#9BACD7"
></path> ></path>
<circle cx="32.408" cy="92.454" r="2.473" fill="#FFFFFF"></circle> <circle cx="32.408" cy="92.454" r="2.473" fill="#FFFFFF"></circle>
<path fill="#FF5758" d="m44.629 78.916 3.366-3.653L64.43 90.408l-3.366 3.653z"></path> <path
<path fill="#FF5758" d="m60.676 75.006 3.512 3.512-15.803 15.803-3.512-3.512z"></path> fill="#FF5758"
d="m44.629 78.916 3.366-3.653L64.43 90.408l-3.366 3.653z"
></path>
<path
fill="#FF5758"
d="m60.676 75.006 3.512 3.512-15.803 15.803-3.512-3.512z"
></path>
</svg> </svg>
</div> </div>
</div> </div>
@ -470,13 +606,13 @@ export default function StepUpload(props: Props) {
className="palmistry-container__take-palm-button" className="palmistry-container__take-palm-button"
disabled={isUpladProcessing} disabled={isUpladProcessing}
active={!isUpladProcessing} active={!isUpladProcessing}
onClick={() => setUploadMenuModalIsOpen(true)} onClick={() => setPalmCameraModalIsOpen(true)}
isProcessing={isUpladProcessing} isProcessing={isUpladProcessing}
> >
{isUpladProcessing && 'Loading photo' || 'Take a picture now'} {(isUpladProcessing && "Loading photo") || "Take a picture now"}
</Button> </Button>
<BiometricData/> <BiometricData />
{uploadMenuModalIsOpen && ( {uploadMenuModalIsOpen && (
<UploadModal <UploadModal
@ -486,10 +622,12 @@ export default function StepUpload(props: Props) {
/> />
)} )}
{isUpladProcessing && <ModalOverlay/>} {isUpladProcessing && <ModalOverlay />}
{recognitionErrorModalIsOpen && ( {recognitionErrorModalIsOpen && (
<PalmRecognitionErrorModal onClose={() => setRecognitionErrorModalIsOpen(false)}/> <PalmRecognitionErrorModal
onClose={() => setRecognitionErrorModalIsOpen(false)}
/>
)} )}
{palmCameraModalIsOpen && ( {palmCameraModalIsOpen && (

42
src/data/products.ts Normal file
View File

@ -0,0 +1,42 @@
import routes from "@/routes";
export enum EProductKeys {
"chat.aura" = "chat.aura",
"moons.pdf.aura" = "moons.pdf.aura",
"default" = "default",
}
interface IProductUrls {
[key: string]: string;
}
export const productUrls: IProductUrls = {
"chat.aura": routes.client.advisorChatPrivate(
"asst_WWkAlT4Ovs6gKRy6VEn9LqNS"
),
"moons.pdf.aura": routes.client.epeSuccessPayment(),
};
interface IPaymentResultPathsOfProducts {
[key: string]: IPaymentResultPathsOfProduct;
}
interface IPaymentResultPathsOfProduct {
success: string;
fail: string;
}
export const paymentResultPathsOfProducts: IPaymentResultPathsOfProducts = {
"moons.pdf.aura": {
success: routes.client.epeSuccessPayment(),
fail: routes.client.epeFailPayment(),
},
"chat.aura": {
success: routes.client.advisorChatSuccessPayment(),
fail: routes.client.advisorChatFailPayment(),
},
default: {
success: routes.client.paymentSuccess(),
fail: routes.client.paymentFail(),
},
};

View File

@ -0,0 +1,170 @@
import { SinglePayment, useApi } from "@/api";
import { User } from "@/api/resources/User";
import { AuthToken } from "@/api/types";
import { productUrls } from "@/data/products";
import routes from "@/routes";
import { getZodiacSignByDate } from "@/services/zodiac-sign";
import { selectors } from "@/store";
import { useCallback, useMemo, useState } from "react";
import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
interface ICreateSinglePaymentProps {
user: User;
token: AuthToken;
targetProductKey: string;
returnUrl: string;
}
interface IErrorSinglePayment {
error?: string;
}
export const useSinglePayment = () => {
const api = useApi();
const navigate = useNavigate();
const [paymentIntent, setPaymentIntent] =
useState<SinglePayment.ResponsePost>();
const [product, setProduct] = useState<SinglePayment.ResponseGet>();
const [error, setError] = useState<IErrorSinglePayment>(
{} as IErrorSinglePayment
);
const [isLoading, setIsLoading] = useState(false);
const { gender } = useSelector(selectors.selectQuestionnaire);
const birthday = useSelector(selectors.selectBirthday);
const getCurrentProduct = useCallback(
async (token: AuthToken, targetProductKey: string) => {
const productsSinglePayment = await api.getSinglePaymentProducts({
token,
});
const currentProduct = productsSinglePayment.find(
(product) => product.key === targetProductKey
);
return currentProduct;
},
[api]
);
const handlerPaymentIntentResult = useCallback(
(paymentIntent: SinglePayment.ResponsePost, type: string) => {
if (!("payment" in paymentIntent)) return;
let status = "failed";
if (paymentIntent.payment.status === "paid") {
status = "succeeded";
}
return navigate(
`${routes.client.paymentResult()}?redirect_status=${status}&redirect_type=${type}`
);
},
[navigate]
);
const checkProductPurchased = useCallback(
async (email: string, productKey: string, token: AuthToken) => {
try {
const purchased = await api.checkProductPurchased({
email,
productKey,
token,
});
if ("active" in purchased && purchased.active) {
return true;
}
return false;
} catch (error) {
console.error(error);
return false;
}
},
[api]
);
const createSinglePayment = useCallback(
async ({
user,
token,
targetProductKey,
returnUrl,
}: ICreateSinglePaymentProps) => {
setIsLoading(true);
const product = await getCurrentProduct(token, targetProductKey);
if (!product) {
setError({ error: "Product not found" });
setIsLoading(false);
return;
}
setProduct(product);
const isPurchased = await checkProductPurchased(
user?.email || "",
targetProductKey,
token
);
if (isPurchased && productUrls[targetProductKey]?.length) {
return navigate(productUrls[targetProductKey]);
}
let _gender = "male";
if (gender.length) {
_gender = gender;
}
if (user.profile.gender?.length) {
_gender = user.profile.gender;
}
const paymentIntent = await api.createSinglePayment({
token,
data: {
user: {
id: `${user?.id}`,
email: user?.email,
name: user.username || "",
sign:
user?.profile?.sign?.sign ||
getZodiacSignByDate(user.profile.birthday || birthday || ""),
age: user?.profile?.age?.years || 1,
gender: _gender,
},
partner: {
sign: null,
age: null,
},
paymentInfo: {
productId: product?.productId || "",
key: product?.key || "",
},
return_url: returnUrl,
},
});
if ("message" in paymentIntent) {
setError({ error: paymentIntent.message });
setIsLoading(false);
return;
}
handlerPaymentIntentResult(paymentIntent, targetProductKey);
setPaymentIntent(paymentIntent);
setIsLoading(false);
return paymentIntent;
},
[
api,
birthday,
checkProductPurchased,
gender,
getCurrentProduct,
handlerPaymentIntentResult,
navigate,
]
);
return useMemo(
() => ({
product,
paymentIntent,
createSinglePayment,
isLoading,
error,
}),
[product, paymentIntent, createSinglePayment, isLoading, error]
);
};

View File

@ -0,0 +1,28 @@
import { getValuesFromRGBString } from "@/services/color-formatter";
import { useEffect } from "react";
export const useSchemeColorByElement = (
element: Element | null,
searchSelectors: string,
dependencies: ReadonlyArray<unknown> = []
) => {
useEffect(() => {
const pageElement = element?.querySelectorAll(searchSelectors)[0];
const scheme = document.querySelector('meta[name="theme-color"]');
if (scheme && !pageElement) {
return scheme.setAttribute("content", "#ffffff");
}
if (!pageElement) return;
let backgroundColor = window.getComputedStyle(pageElement)?.backgroundColor;
const colorScheme = getValuesFromRGBString(backgroundColor);
if (colorScheme?.a === 0) {
backgroundColor = "#ffffff";
}
if (scheme && backgroundColor.length) {
return scheme.setAttribute("content", backgroundColor);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [element, ...dependencies]);
return null;
};

View File

@ -124,7 +124,7 @@ const routes = {
// Advisors // Advisors
advisors: () => [host, "advisors"].join("/"), advisors: () => [host, "advisors"].join("/"),
advisorChat: (id: number) => [host, "advisors", id].join("/"), advisorChat: (id: string) => [host, "advisors", id].join("/"),
// Email - Pay - Email // Email - Pay - Email
epeGender: () => [host, "epe", "gender"].join("/"), epeGender: () => [host, "epe", "gender"].join("/"),
epeBirthdate: () => [host, "epe", "birthdate"].join("/"), epeBirthdate: () => [host, "epe", "birthdate"].join("/"),
@ -132,8 +132,26 @@ const routes = {
epeSuccessPayment: () => [host, "epe", "success-payment"].join("/"), epeSuccessPayment: () => [host, "epe", "success-payment"].join("/"),
epeFailPayment: () => [host, "epe", "fail-payment"].join("/"), epeFailPayment: () => [host, "epe", "fail-payment"].join("/"),
// Advisor short path
advisorChatGender: () => [host, "advisor-chat", "gender"].join("/"),
advisorChatBirthdate: () => [host, "advisor-chat", "birthdate"].join("/"),
advisorChatBirthtime: () => [host, "advisor-chat", "birthtime"].join("/"),
advisorChatBirthPlace: () =>
[host, "advisor-chat", "birth-place"].join("/"),
advisorChatPayment: () => [host, "advisor-chat", "payment"].join("/"),
advisorChatSuccessPayment: () =>
[host, "advisor-chat", "success-payment"].join("/"),
advisorChatFailPayment: () =>
[host, "advisor-chat", "fail-payment"].join("/"),
advisorChatPrivate: (id?: string) =>
[host, "advisor-chat-private", id].join("/"),
singlePaymentShortPath: (productId?: string) =>
[host, "single-payment", productId].join("/"),
getInformationPartner: () => [host, "get-information-partner"].join("/"), getInformationPartner: () => [host, "get-information-partner"].join("/"),
loadingPage: () => [host, "loading-page"].join("/"),
notFound: () => [host, "404"].join("/"), notFound: () => [host, "404"].join("/"),
}, },
server: { server: {
@ -194,10 +212,17 @@ const routes = {
dApiTestPaymentProducts: () => dApiTestPaymentProducts: () =>
[dApiHost, "payment", "test", "products"].join("/"), [dApiHost, "payment", "test", "products"].join("/"),
dApiPaymentCheckout: () => [dApiHost, "payment", "checkout"].join("/"), dApiPaymentCheckout: () => [dApiHost, "payment", "checkout"].join("/"),
dApiCheckProductPurchased: (productKey: string, email: string) =>
[dApiHost, "payment", "products", `${productKey}?email=${email}`].join(
"/"
),
assistants: () => [apiHost, prefix, "ai", "assistants.json"].join("/"), assistants: () => [apiHost, prefix, "ai", "assistants.json"].join("/"),
setExternalChatIdAssistants: (chatId: string) => setExternalChatIdAssistants: (chatId: string) =>
[apiHost, prefix, "ai", "assistants", chatId, "chats.json"].join("/"), [apiHost, prefix, "ai", "assistants", chatId, "chats.json"].join("/"),
// Palmistry
getPalmistryLines: () =>
["https://api.aura.witapps.us", "palmistry", "lines"].join("/"),
}, },
openAi: { openAi: {
createThread: () => [openAIHost, openAiPrefix, "threads"].join("/"), createThread: () => [openAIHost, openAiPrefix, "threads"].join("/"),
@ -230,6 +255,9 @@ export const entrypoints = [
routes.client.trialChoice(), routes.client.trialChoice(),
routes.client.palmistry(), routes.client.palmistry(),
routes.client.advisors(), routes.client.advisors(),
routes.client.advisorChatGender(),
routes.client.advisorChatSuccessPayment(),
routes.client.advisorChatFailPayment(),
]; ];
export const isEntrypoint = (path: string) => entrypoints.includes(path); export const isEntrypoint = (path: string) => entrypoints.includes(path);
export const isNotEntrypoint = (path: string) => !isEntrypoint(path); export const isNotEntrypoint = (path: string) => !isEntrypoint(path);
@ -318,11 +346,13 @@ export const withoutFooterRoutes = [
routes.client.advisors(), routes.client.advisors(),
routes.client.epeSuccessPayment(), routes.client.epeSuccessPayment(),
routes.client.getInformationPartner(), routes.client.getInformationPartner(),
routes.client.advisorChatBirthPlace(),
]; ];
export const withoutFooterPartOfRoutes = [ export const withoutFooterPartOfRoutes = [
routes.client.questionnaire(), routes.client.questionnaire(),
routes.client.advisors(), routes.client.advisors(),
routes.client.advisorChatPrivate(),
]; ];
export const hasNoFooter = (path: string) => { export const hasNoFooter = (path: string) => {
@ -393,6 +423,7 @@ export const withoutHeaderRoutes = [
routes.client.advisors(), routes.client.advisors(),
routes.client.epeSuccessPayment(), routes.client.epeSuccessPayment(),
routes.client.getInformationPartner(), routes.client.getInformationPartner(),
routes.client.advisorChatPrivate(),
]; ];
export const hasNoHeader = (path: string) => { export const hasNoHeader = (path: string) => {
let result = true; let result = true;

View File

@ -0,0 +1,44 @@
const hexToRgb = (hex: string) => {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, function (_m, r, g, b) {
return r + r + g + g + b + b;
});
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
};
interface IRGBValues {
r: number;
g: number;
b: number;
}
const rgbToHex = ({ r, g, b }: IRGBValues) => {
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
};
export const getValuesFromRGBString = (rgb: string) => {
const rgbValues = rgb.match(/\d+/g);
if (rgbValues) {
return {
r: Number(rgbValues[0]),
g: Number(rgbValues[1]),
b: Number(rgbValues[2]),
a: Number(rgbValues[3] || 1),
};
}
return null;
};
export class ColorFormatter {
public static rgbToHex = rgbToHex;
public static hexToRgb = hexToRgb;
}

View File

@ -1,10 +1,11 @@
import { User } from "@/api/resources/User"; import { User } from "@/api/resources/User";
import { getZodiacSignByDate } from "../zodiac-sign"; import { getZodiacSignByDate } from "../zodiac-sign";
import { ApiContextValue } from "@/api"; import { ApiContextValue } from "@/api";
import { IPaymentInfo } from "@/api/resources/SinglePayment";
export const createSinglePayment = async ( export const createSinglePayment = async (
user: User, user: User,
productId: string, paymentInfo: IPaymentInfo,
token: string, token: string,
email: string, email: string,
name: string | null, name: string | null,
@ -28,9 +29,7 @@ export const createSinglePayment = async (
sign: "partner_cancer", sign: "partner_cancer",
age: 26, age: 26,
}, },
paymentInfo: { paymentInfo,
productId,
},
return_url: returnUrl, return_url: returnUrl,
}, },
}); });

View File

@ -46,6 +46,7 @@ import userConfig, {
actions as userConfigActions, actions as userConfigActions,
selectUserDeviceType, selectUserDeviceType,
selectIsShowTryApp, selectIsShowTryApp,
selectIsForceShortPath,
} from "./userConfig"; } from "./userConfig";
import compatibilities, { import compatibilities, {
actions as compatibilitiesActions, actions as compatibilitiesActions,
@ -62,6 +63,10 @@ import {
selectUserCallbacksDescription, selectUserCallbacksDescription,
selectUserCallbacksPrevStat, selectUserCallbacksPrevStat,
} from "./userCallbacks"; } from "./userCallbacks";
import palmistry, {
actions as palmistryActions,
selectPalmistryLines,
} from "./palmistry";
const preloadedState = loadStore(); const preloadedState = loadStore();
export const actions = { export const actions = {
@ -79,6 +84,7 @@ export const actions = {
onboardingConfig: onboardingConfigActions, onboardingConfig: onboardingConfigActions,
questionnaire: questionnaireActions, questionnaire: questionnaireActions,
userConfig: userConfigActions, userConfig: userConfigActions,
palmistry: palmistryActions,
reset: createAction("reset"), reset: createAction("reset"),
}; };
export const selectors = { export const selectors = {
@ -106,7 +112,9 @@ export const selectors = {
selectQuestionnaire, selectQuestionnaire,
selectUserDeviceType, selectUserDeviceType,
selectIsShowTryApp, selectIsShowTryApp,
selectIsForceShortPath,
selectOpenAiToken, selectOpenAiToken,
selectPalmistryLines,
...formSelectors, ...formSelectors,
}; };
@ -125,6 +133,7 @@ export const reducer = combineReducers({
onboardingConfig, onboardingConfig,
questionnaire, questionnaire,
userConfig, userConfig,
palmistry,
}); });
export type RootState = ReturnType<typeof reducer>; export type RootState = ReturnType<typeof reducer>;

29
src/store/palmistry.ts Normal file
View File

@ -0,0 +1,29 @@
import { IPalmistryLine } from "@/api/resources/Palmistry";
import { createSlice, createSelector } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";
interface IPalmistry {
lines: IPalmistryLine[];
}
const initialState: IPalmistry = {
lines: [],
};
const palmistrySlice = createSlice({
name: "palmistry",
initialState,
reducers: {
update(state, action: PayloadAction<Partial<IPalmistry>>) {
return { ...state, ...action.payload };
},
},
extraReducers: (builder) => builder.addCase("reset", () => initialState),
});
export const { actions } = palmistrySlice;
export const selectPalmistryLines = createSelector(
(state: { palmistry: IPalmistry }) => state.palmistry.lines,
(palmistry) => palmistry
);
export default palmistrySlice.reducer;

View File

@ -9,11 +9,13 @@ export enum EUserDeviceType {
interface IUserConfig { interface IUserConfig {
deviceType: EUserDeviceType; deviceType: EUserDeviceType;
isShowTryApp: boolean; isShowTryApp: boolean;
isForceShortPath: boolean;
} }
const initialState: IUserConfig = { const initialState: IUserConfig = {
deviceType: EUserDeviceType.ios, deviceType: EUserDeviceType.ios,
isShowTryApp: false, isShowTryApp: false,
isForceShortPath: false,
}; };
const userConfigSlice = createSlice({ const userConfigSlice = createSlice({
@ -31,6 +33,10 @@ const userConfigSlice = createSlice({
state.isShowTryApp = action.payload; state.isShowTryApp = action.payload;
return state; return state;
}, },
addIsForceShortPath(state, action: PayloadAction<boolean>) {
state.isForceShortPath = action.payload;
return state;
},
}, },
extraReducers: (builder) => builder.addCase("reset", () => initialState), extraReducers: (builder) => builder.addCase("reset", () => initialState),
}); });
@ -44,4 +50,8 @@ export const selectIsShowTryApp = createSelector(
(state: { userConfig: IUserConfig }) => state.userConfig.isShowTryApp, (state: { userConfig: IUserConfig }) => state.userConfig.isShowTryApp,
(userConfig) => userConfig (userConfig) => userConfig
); );
export const selectIsForceShortPath = createSelector(
(state: { userConfig: IUserConfig }) => state.userConfig.isForceShortPath,
(userConfig) => userConfig
);
export default userConfigSlice.reducer; export default userConfigSlice.reducer;