add short path for advisor chat

This commit is contained in:
Денис Катаев 2024-04-09 16:46:50 +00:00 committed by Daniil Chemerkin
parent d381440556
commit ef239432d5
26 changed files with 686 additions and 189 deletions

View File

@ -27,6 +27,7 @@ import {
Assistants,
OpenAI,
SinglePayment,
Products,
} from './resources'
const api = {
@ -69,7 +70,8 @@ const api = {
getListRuns: createMethod<OpenAI.PayloadGetListRuns, OpenAI.ResponseGetListRuns>(OpenAI.createRequest),
// Single payment
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),
}
export type ApiContextValue = typeof api

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

@ -38,7 +38,7 @@ export interface ResponseGet {
currency: string;
}
export interface ResponsePost {
interface ResponsePostNewPaymentData {
paymentIntent: {
status: string;
data: {
@ -60,13 +60,23 @@ export interface ResponsePost {
};
}
export interface ResponsePostExistPaymentData {
interface ResponsePostExistPaymentData {
payment: {
status: string;
invoiceId: string;
};
}
interface ResponsePostError {
status: string;
message: string;
}
export type ResponsePost =
| ResponsePostNewPaymentData
| ResponsePostExistPaymentData
| ResponsePostError;
export const createRequestPost = ({ data, token }: PayloadPost): Request => {
const url = new URL(routes.server.dApiPaymentCheckout());
const body = JSON.stringify(data);

View File

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

View File

@ -111,6 +111,8 @@ import SuccessPaymentPage from "../pages/PaymentWithEmailPage/ResultPayment/Succ
import FailPaymentPage from "../pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage";
import { useSchemeColorByElement } from "@/hooks/useSchemeColorByElement";
import GetInformationPartnerPage from "../pages/GetInformationPartner";
import BirthPlacePage from "../pages/BirthPlacePage";
import LoadingPage from "../pages/LoadingPage";
const isProduction = import.meta.env.MODE === "production";
@ -252,13 +254,14 @@ function App(): JSX.Element {
return (
<Routes>
<Route element={<Layout setIsSpecialOfferOpen={setIsSpecialOfferOpen} />}>
<Route path={routes.client.loadingPage()} element={<LoadingPage />} />
{/* Email - Pay - Email */}
<Route path={routes.client.epeGender()} element={<GenderPage />} />
<Route path={routes.client.epeBirthdate()} element={<BirthdayPage />} />
<Route
{/* <Route
path={routes.client.epePayment()}
element={<PaymentWithEmailPage />}
/>
/> */}
<Route
path={routes.client.epeSuccessPayment()}
element={<SuccessPaymentPage />}
@ -269,6 +272,68 @@ function App(): JSX.Element {
/>
{/* Email - Pay - Email */}
{/* Advisor short path */}
<Route
element={
<CheckPurchasedSingleProductOutlet
productKey="chat.aura"
isProductPage={false}
failedUrl={routes.client.advisorChatPrivate(
"asst_WWkAlT4Ovs6gKRy6VEn9LqNS"
)}
/>
}
>
<Route
path={routes.client.advisorChatGender()}
element={<GenderPage />}
/>
<Route
path={routes.client.advisorChatBirthdate()}
element={<BirthdayPage />}
/>
<Route
path={routes.client.advisorChatBirthtime()}
element={<BirthtimePage />}
/>
<Route
path={routes.client.advisorChatBirthPlace()}
element={<BirthPlacePage />}
/>
<Route
path={routes.client.advisorChatSuccessPayment()}
element={<SuccessPaymentPage />}
/>
<Route
path={routes.client.advisorChatFailPayment()}
element={<FailPaymentPage />}
/>
</Route>
<Route
element={
<CheckPurchasedSingleProductOutlet
isProductPage={true}
failedUrl={routes.client.advisorChatGender()}
productKey="chat.aura"
/>
}
>
<Route path={`${routes.client.advisorChatPrivate()}`}>
<Route path=":id" element={<AdvisorChatPage />} />
</Route>
</Route>
{/* Advisor short path */}
{/* Single Payment Page Short Path */}
<Route
path={routes.client.singlePaymentShortPath()}
element={<PaymentWithEmailPage />}
>
<Route path=":productId" element={<PaymentWithEmailPage />} />
</Route>
{/* Single Payment Page Short Path */}
{/* Test Routes Start */}
<Route path={routes.client.notFound()} element={<NotFoundPage />} />
<Route path={routes.client.gender()} element={<GenderPage />}>
@ -692,6 +757,63 @@ function Layout({ setIsSpecialOfferOpen }: LayoutProps): JSX.Element {
);
}
interface ICheckPurchasedSingleProductOutletProps {
productKey: string;
isProductPage: boolean;
failedUrl: string;
}
function CheckPurchasedSingleProductOutlet({
productKey,
isProductPage,
failedUrl,
}: ICheckPurchasedSingleProductOutletProps): JSX.Element {
const { user, token } = useAuth();
const api = useApi();
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 (!data || isPending) {
return <LoadingPage />;
}
if (
isProductPage &&
(!("active" in data) || !data.active || !token.length || !user?.email)
) {
return <Navigate to={failedUrl} replace={true} />;
}
if (!isProductPage && data && "active" in data && data.active) {
return <Navigate to={failedUrl} replace={true} />;
}
return <Outlet />;
}
function AuthorizedUserOutlet(): JSX.Element {
const status = useSelector(selectors.selectStatus);
const { user } = useAuth();

View File

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

View File

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

View File

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

View File

@ -12,7 +12,6 @@ import { selectors } from "@/store";
import { useCallback, useState } from "react";
import {
ResponsePost,
ResponsePostExistPaymentData,
} from "@/api/resources/SinglePayment";
import { createSinglePayment } from "@/services/singlePayment";
import Modal from "@/components/Modal";
@ -27,7 +26,7 @@ function AddConsultationPage() {
const tokenFromStore = useSelector(selectors.selectToken);
const [isLoading, setIsLoading] = useState(false);
const [paymentIntent, setPaymentIntent] = useState<
ResponsePost | ResponsePostExistPaymentData | null
ResponsePost | null
>(null);
const [isError, setIsError] = useState(false);
const returnUrl = `${window.location.protocol}//${

View File

@ -11,7 +11,6 @@ import PaymentAddress from "../../components/PaymentAddress";
import { createSinglePayment } from "@/services/singlePayment";
import {
ResponsePost,
ResponsePostExistPaymentData,
} from "@/api/resources/SinglePayment";
import { useAuth } from "@/auth";
import { useSelector } from "react-redux";
@ -28,7 +27,7 @@ function AddReportPage() {
const api = useApi();
const tokenFromStore = useSelector(selectors.selectToken);
const [paymentIntent, setPaymentIntent] = useState<
ResponsePost | ResponsePostExistPaymentData | null
ResponsePost | null
>(null);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);

View File

@ -18,7 +18,6 @@ import { createSinglePayment } from "@/services/singlePayment";
import Loader, { LoaderColor } from "@/components/Loader";
import {
ResponsePost,
ResponsePostExistPaymentData,
} from "@/api/resources/SinglePayment";
import Modal from "@/components/Modal";
import { getPriceCentsToDollars } from "@/services/price";
@ -42,7 +41,7 @@ function UnlimitedReadingsPage() {
const tokenFromStore = useSelector(selectors.selectToken);
const [isLoading, setIsLoading] = useState(false);
const [paymentIntent, setPaymentIntent] = useState<
ResponsePost | ResponsePostExistPaymentData | null
ResponsePost | null
>(null);
const [isError, setIsError] = useState(false);
const returnUrl = `${window.location.protocol}//${

View File

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

View File

@ -16,6 +16,7 @@ import useDetectScroll from "@smakss/react-scroll-direction";
import { getZodiacSignByDate } from "@/services/zodiac-sign";
function AdvisorChatPage() {
const isPrivateChat = window.location.href.includes("/advisor-chat-private/");
const { id } = useParams();
const api = useApi();
const navigate = useNavigate();
@ -25,7 +26,9 @@ function AdvisorChatPage() {
const birthdate = useSelector(selectors.selectBirthdate);
const zodiacSign = getZodiacSignByDate(birthdate);
const { username } = useSelector(selectors.selectUser);
const { gender } = useSelector(selectors.selectQuestionnaire);
const { gender, birthtime, birthPlace } = useSelector(
selectors.selectQuestionnaire
);
const [assistant, setAssistant] = useState<IAssistant>();
const [messageText, setMessageText] = useState("");
const [textareaRows, setTextareaRows] = useState(1);
@ -88,7 +91,7 @@ function AdvisorChatPage() {
idAssistant: string | number
) => {
const currentAssistant = aiAssistants.find(
(a) => a.id === Number(idAssistant)
(a) => a.external_id === idAssistant
);
return currentAssistant;
};
@ -196,10 +199,17 @@ function AdvisorChatPage() {
return run;
};
const createMessage = async (messageText: string, threadId: string) => {
const getContentMessage = (messageText: string) => {
const content = `#USER INFO: zodiac sign - ${zodiacSign}; gender - ${gender}; birthdate - ${birthdate}; name - ${
username || "unknown"
};# ${messageText}`;
}; birthtime - ${birthtime || "unknown"}; birthPlace - ${
birthPlace || "unknown"
}# ${messageText}`;
return content;
};
const createMessage = async (messageText: string, threadId: string) => {
const content = getContentMessage(messageText);
const message = await api.createMessage({
token: openAiToken,
method: "POST",
@ -260,7 +270,8 @@ function AdvisorChatPage() {
threadId = assistant.external_chat_id;
assistantId = assistant.external_id || "";
} else {
const thread = await createThread(messageText);
const content = getContentMessage(messageText);
const thread = await createThread(content);
threadId = thread.id;
assistantId = assistant?.external_id || "";
@ -321,6 +332,7 @@ function AdvisorChatPage() {
avatar={assistant?.photo?.th2x || ""}
classNameContainer={styles["header-container"]}
clickBackButton={() => navigate(-1)}
hasBackButton={!isPrivateChat}
/>
)}
{!!messages.length && (

View File

@ -31,7 +31,7 @@ function Advisors() {
useApiCall<Assistants.Response>(loadData);
const handleAdvisorClick = (assistant: IAssistant) => {
navigate(routes.client.advisorChat(assistant.id));
navigate(routes.client.advisorChat(assistant.external_id));
};
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

@ -23,6 +23,9 @@ function GenderPage(): JSX.Element {
if (pathName.includes("/epe/gender")) {
return navigate(routes.client.epeBirthdate());
}
if (pathName.includes("/advisor-chat/gender")) {
return navigate(routes.client.advisorChatBirthdate());
}
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 {
const { t } = useTranslation();
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 (
<section className={`${styles.page} page`}>

View File

@ -1,9 +1,17 @@
import { useTranslation } from "react-i18next";
import styles from "./styles.module.css";
import Title from "@/components/Title";
import MainButton from "@/components/MainButton";
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
function SuccessPaymentPage(): JSX.Element {
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 (
<section className={`${styles.page} page`}>
@ -13,9 +21,21 @@ function SuccessPaymentPage(): JSX.Element {
style={{ minHeight: "98px" }}
/>
<div className={styles.text}>
<Title variant="h1">The information has been sent to your email</Title>
<Title variant="h1">{titleText}</Title>
<p>{t("auweb.pay_good.text1")}</p>
</div>
{isAdvisorChat && (
<MainButton
className={styles.button}
onClick={() =>
navigate(
routes.client.advisorChatPrivate("asst_WWkAlT4Ovs6gKRy6VEn9LqNS")
)
}
>
{t("auweb.pay_good.button")}
</MainButton>
)}
</section>
);
}

View File

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

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

@ -0,0 +1,35 @@
import routes from "@/routes";
interface IProductUrls {
[key: string]: string;
}
export const productUrls: IProductUrls = {
"chat.aura": routes.client.advisorChatPrivate(
"asst_WWkAlT4Ovs6gKRy6VEn9LqNS"
),
};
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,156 @@
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 &&
productUrls[productKey].length
) {
return navigate(productUrls[productKey]);
}
} catch (error) {
console.error(error);
}
},
[api, navigate]
);
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);
await checkProductPurchased(user?.email || "", targetProductKey, token);
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: user.profile.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,
]
);
return useMemo(
() => ({
product,
paymentIntent,
createSinglePayment,
isLoading,
error,
}),
[product, paymentIntent, createSinglePayment, isLoading, error]
);
};

View File

@ -8,7 +8,6 @@ export const useSchemeColorByElement = (
) => {
useEffect(() => {
const pageElement = element?.querySelectorAll(searchSelectors)[0];
console.log("pageElement", pageElement);
const scheme = document.querySelector('meta[name="theme-color"]');
if (scheme && !pageElement) {
@ -20,7 +19,6 @@ export const useSchemeColorByElement = (
if (colorScheme?.a === 0) {
backgroundColor = "#ffffff";
}
console.log("backgroundColor", backgroundColor);
if (scheme && backgroundColor.length) {
return scheme.setAttribute("content", backgroundColor);
}

View File

@ -124,7 +124,7 @@ const routes = {
// Advisors
advisors: () => [host, "advisors"].join("/"),
advisorChat: (id: number) => [host, "advisors", id].join("/"),
advisorChat: (id: string) => [host, "advisors", id].join("/"),
// Email - Pay - Email
epeGender: () => [host, "epe", "gender"].join("/"),
epeBirthdate: () => [host, "epe", "birthdate"].join("/"),
@ -132,8 +132,26 @@ const routes = {
epeSuccessPayment: () => [host, "epe", "success-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("/"),
loadingPage: () => [host, "loading-page"].join("/"),
notFound: () => [host, "404"].join("/"),
},
server: {
@ -194,6 +212,10 @@ const routes = {
dApiTestPaymentProducts: () =>
[dApiHost, "payment", "test", "products"].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("/"),
setExternalChatIdAssistants: (chatId: string) =>
@ -230,6 +252,9 @@ export const entrypoints = [
routes.client.trialChoice(),
routes.client.palmistry(),
routes.client.advisors(),
routes.client.advisorChatGender(),
routes.client.advisorChatSuccessPayment(),
routes.client.advisorChatFailPayment(),
];
export const isEntrypoint = (path: string) => entrypoints.includes(path);
export const isNotEntrypoint = (path: string) => !isEntrypoint(path);
@ -318,11 +343,13 @@ export const withoutFooterRoutes = [
routes.client.advisors(),
routes.client.epeSuccessPayment(),
routes.client.getInformationPartner(),
routes.client.advisorChatBirthPlace(),
];
export const withoutFooterPartOfRoutes = [
routes.client.questionnaire(),
routes.client.advisors(),
routes.client.advisorChatPrivate(),
];
export const hasNoFooter = (path: string) => {
@ -393,6 +420,7 @@ export const withoutHeaderRoutes = [
routes.client.advisors(),
routes.client.epeSuccessPayment(),
routes.client.getInformationPartner(),
routes.client.advisorChatPrivate(),
];
export const hasNoHeader = (path: string) => {
let result = true;