(SinglePayment.createRequestPost),
}
export type ApiContextValue = typeof api
diff --git a/src/api/resources/Assistants.ts b/src/api/resources/Assistants.ts
new file mode 100644
index 0000000..1dcf735
--- /dev/null
+++ b/src/api/resources/Assistants.ts
@@ -0,0 +1,65 @@
+import routes from "@/routes";
+import { getAuthHeaders } from "../utils";
+
+export interface Payload {
+ token: string;
+}
+
+export interface Response {
+ ai_assistants: IAssistant[];
+}
+
+export interface IAssistant {
+ id: number;
+ name: string;
+ external_id: string;
+ external_chat_id: string | null;
+ photo: {
+ th: string;
+ th2x: string;
+ lg: string;
+ };
+ photo_mime_type: string;
+ created_at: string;
+ updated_at: string;
+ expirience: string;
+ rating: string;
+ stars: number;
+}
+
+export interface PayloadSetExternalChatId extends Payload {
+ chatId: string;
+ ai_assistant_chat: {
+ external_id: string;
+ };
+}
+
+export interface ResponseSetExternalChatId {
+ ai_assistant_chat: {
+ id: number;
+ assistant_id: number;
+ external_id: string;
+ created_at: string;
+ updated_at: string;
+ };
+}
+
+export const createRequest = ({ token }: Payload): Request => {
+ const url = new URL(routes.server.assistants());
+
+ return new Request(url, { method: "GET", headers: getAuthHeaders(token) });
+};
+
+export const createRequestSetExternalChatId = ({
+ token,
+ ai_assistant_chat,
+ chatId,
+}: PayloadSetExternalChatId) => {
+ const url = new URL(routes.server.setExternalChatIdAssistants(chatId));
+ const body = JSON.stringify({ ai_assistant_chat });
+ return new Request(url, {
+ method: "POST",
+ headers: getAuthHeaders(token),
+ body,
+ });
+};
diff --git a/src/api/resources/OpenAI.ts b/src/api/resources/OpenAI.ts
new file mode 100644
index 0000000..4697dc5
--- /dev/null
+++ b/src/api/resources/OpenAI.ts
@@ -0,0 +1,164 @@
+import { getAuthOpenAIHeaders } from "../utils";
+
+interface IDefaultPayload {
+ token: string;
+ method: "POST" | "GET";
+ path: string;
+}
+
+interface IMessagePayload {
+ role: string;
+ content: string;
+ file_ids?: string[];
+}
+
+export interface IMessage {
+ id: string;
+ object: "thread.message" | string;
+ created_at: number;
+ thread_id: string;
+ role: "user" | string;
+ content: IMessageContent[];
+ file_ids: unknown[];
+ assistant_id: null | unknown;
+ run_id: null | unknown;
+ metadata: object;
+}
+
+interface IMessageContent {
+ type: "text" | string;
+ text: {
+ value: string;
+ annotations: unknown[];
+ };
+}
+
+// Create thread
+export interface PayloadCreateThread extends IDefaultPayload {
+ messages: IMessagePayload[];
+}
+
+export interface ResponseCreateThread {
+ id: string;
+ object: string;
+ created_at: number;
+ metadata: object;
+}
+// Create thread end
+
+// Create message
+export interface PayloadCreateMessage extends IDefaultPayload {
+ role: "user";
+ content: string;
+}
+
+export type ResponseCreateMessage = IMessage;
+// Create message end
+
+// Get list messages
+export interface PayloadGetListMessages extends IDefaultPayload {
+ QueryParams?: {
+ limit?: number;
+ order?: "asc" | "desc";
+ after?: string;
+ before?: string;
+ };
+}
+
+export interface ResponseGetListMessages {
+ object: "list" | string;
+ data: IMessage[];
+ first_id: string;
+ last_id: string;
+ has_more: boolean;
+}
+// Get list messages end
+
+// Run thread
+interface ITool {
+ type: "code_interpreter" | string;
+}
+
+interface IUsage {
+ prompt_tokens: number;
+ completion_tokens: number;
+ total_tokens: number;
+}
+
+export interface PayloadRunThread extends IDefaultPayload {
+ assistant_id: string;
+}
+
+export interface ResponseRunThread {
+ id: string;
+ object: "thread.run" | string;
+ created_at: number;
+ assistant_id: string;
+ thread_id: string;
+ status: "queued" | string;
+ started_at: number;
+ expires_at: null | unknown;
+ cancelled_at: null | unknown;
+ failed_at: null | unknown;
+ completed_at: number;
+ last_error: null | unknown;
+ model: "gpt-4" | string;
+ instructions: null | unknown;
+ tools: ITool[];
+ file_ids: string[];
+ metadata: object;
+ usage: null | IUsage;
+}
+// Run thread end
+
+// Get status run thread
+export type PayloadGetStatusRunThread = IDefaultPayload;
+
+export type ResponseGetStatusRunThread = ResponseRunThread;
+// Get status run thread end
+
+// Get list runs
+export type PayloadGetListRuns = IDefaultPayload;
+
+export interface ResponseGetListRuns {
+ object: "list" | string;
+ data: ResponseRunThread[];
+ first_id: string;
+ last_id: string;
+ has_more: boolean;
+}
+// Get list runs end
+
+export type Payload =
+ | PayloadCreateThread
+ | PayloadCreateMessage
+ | PayloadGetListMessages
+ | PayloadRunThread
+ | PayloadGetStatusRunThread;
+
+export function createRequest({
+ token,
+ method,
+ path,
+ ...data
+}: Payload): Request {
+ const url = new URL(path);
+ const body = JSON.stringify({ ...data });
+ if ("QueryParams" in data && data.QueryParams) {
+ for (const [key, value] of Object.entries(data.QueryParams)) {
+ url.searchParams.set(key, String(value));
+ }
+ }
+ if (method === "GET") {
+ return new Request(url, {
+ method: method,
+ headers: getAuthOpenAIHeaders(token),
+ });
+ }
+
+ return new Request(url, {
+ method: method,
+ headers: getAuthOpenAIHeaders(token),
+ body,
+ });
+}
diff --git a/src/api/resources/SinglePayment.ts b/src/api/resources/SinglePayment.ts
new file mode 100644
index 0000000..e7c63cd
--- /dev/null
+++ b/src/api/resources/SinglePayment.ts
@@ -0,0 +1,79 @@
+import routes from "@/routes";
+import { getAuthHeaders } from "../utils";
+
+interface Payload {
+ token: string;
+}
+
+export type PayloadGet = Payload;
+
+export interface PayloadPost extends Payload {
+ data: {
+ user: {
+ id: string;
+ email: string;
+ name: string;
+ sign: string;
+ age: number;
+ };
+ partner: {
+ sign: string | null;
+ age: number | null;
+ };
+ paymentInfo: {
+ productId: string;
+ };
+ return_url: string;
+ };
+}
+
+export interface ResponseGet {
+ key: string;
+ productId: string;
+ amount: number;
+ currency: string;
+}
+
+export interface ResponsePost {
+ paymentIntent: {
+ status: string;
+ data: {
+ client_secret: string;
+ paymentIntentId: string;
+ return_url?: string;
+ public_key: string;
+ product: {
+ id: string;
+ name: string;
+ description: null | string;
+ price: {
+ id: string;
+ unit_amount: number;
+ currency: string;
+ };
+ };
+ };
+ };
+}
+
+export interface ResponsePostExistPaymentData {
+ payment: {
+ status: string;
+ invoiceId: string;
+ };
+}
+
+export const createRequestPost = ({ data, token }: PayloadPost): Request => {
+ const url = new URL(routes.server.dApiPaymentCheckout());
+ const body = JSON.stringify(data);
+ return new Request(url, {
+ method: "POST",
+ headers: getAuthHeaders(token),
+ body,
+ });
+};
+
+export const createRequestGet = ({ token }: PayloadGet): Request => {
+ const url = new URL(routes.server.dApiTestPaymentProducts());
+ return new Request(url, { method: "GET", headers: getAuthHeaders(token) });
+};
diff --git a/src/api/resources/index.ts b/src/api/resources/index.ts
index bdd0dcf..6c11bf3 100644
--- a/src/api/resources/index.ts
+++ b/src/api/resources/index.ts
@@ -22,3 +22,6 @@ export * as GoogleAuth from "./GoogleAuth";
export * as SubscriptionPlans from "./SubscriptionPlans";
export * as AppleAuth from "./AppleAuth";
export * as AIRequestsV2 from "./AIRequestsV2";
+export * as Assistants from "./Assistants";
+export * as OpenAI from "./OpenAI";
+export * as SinglePayment from "./SinglePayment";
diff --git a/src/api/utils.ts b/src/api/utils.ts
index 6464a96..39f80a1 100644
--- a/src/api/utils.ts
+++ b/src/api/utils.ts
@@ -1,34 +1,48 @@
-import { AuthToken } from './types'
-import { ErrorResponse, isErrorResponse, ApiError, buildUnknownError } from './errors'
+import { AuthToken } from "./types";
+import {
+ ErrorResponse,
+ isErrorResponse,
+ ApiError,
+ buildUnknownError,
+} from "./errors";
export function createMethod(createRequest: (payload: P) => Request) {
return async (payload: P): Promise => {
- const request = createRequest(payload)
- const response = await fetch(request)
- const data: R | ErrorResponse = await response.json()
- const statusCode = response.status
+ const request = createRequest(payload);
+ const response = await fetch(request);
+ const data: R | ErrorResponse = await response.json();
+ const statusCode = response.status;
if (!response.ok) {
- const body = isErrorResponse(data) ? data : { error: buildUnknownError(statusCode) }
- throw new ApiError({ body, statusCode })
+ const body = isErrorResponse(data)
+ ? data
+ : { error: buildUnknownError(statusCode) };
+ throw new ApiError({ body, statusCode });
}
if (isErrorResponse(data)) {
- throw new ApiError({ body: data, statusCode })
+ throw new ApiError({ body: data, statusCode });
}
- return data
- }
+ return data;
+ };
}
export function getBaseHeaders(): Headers {
return new Headers({
- 'Content-Type': 'application/json',
- })
+ "Content-Type": "application/json",
+ });
}
export function getAuthHeaders(token: AuthToken): Headers {
- const headers = getBaseHeaders()
- headers.append('Authorization', `Bearer ${token}`)
- return headers
+ const headers = getBaseHeaders();
+ headers.append("Authorization", `Bearer ${token}`);
+ return headers;
+}
+
+export function getAuthOpenAIHeaders(token: AuthToken): Headers {
+ const headers = getBaseHeaders();
+ headers.append("Authorization", `Bearer ${token}`);
+ headers.append("OpenAI-Beta", "assistants=v1");
+ return headers;
}
diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx
index decbb9a..7cc558b 100755
--- a/src/components/App/index.tsx
+++ b/src/components/App/index.tsx
@@ -104,6 +104,11 @@ import AddReportPage from "../pages/AdditionalPurchases/pages/AddReport";
import UnlimitedReadingsPage from "../pages/AdditionalPurchases/pages/UnlimitedReadings";
import AddConsultationPage from "../pages/AdditionalPurchases/pages/AddConsultation";
import StepsManager from "@/components/palmistry/steps-manager/steps-manager";
+import Advisors from "../pages/Advisors";
+import AdvisorChatPage from "../pages/AdvisorChat";
+import PaymentWithEmailPage from "../pages/PaymentWithEmailPage";
+import SuccessPaymentPage from "../pages/PaymentWithEmailPage/ResultPayment/SuccessPaymentPage";
+import FailPaymentPage from "../pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage";
const isProduction = import.meta.env.MODE === "production";
@@ -122,6 +127,15 @@ function App(): JSX.Element {
const [searchParams] = useSearchParams();
const jwtToken = searchParams.get("token");
+ useEffect(() => {
+ // api.getAppConfig({ bundleId: "auraweb" }),
+ dispatch(
+ actions.siteConfig.update({
+ openAiToken: "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",
+ })
+ );
+ }, [dispatch]);
+
useEffect(() => {
if (!isProduction) return;
ReactGA.send({
@@ -216,6 +230,14 @@ function App(): JSX.Element {
return (
}>
+ {/* Email - Pay - Email */}
+ } />
+ } />
+ } />
+ } />
+ } />
+ {/* Email - Pay - Email */}
+
{/* Test Routes Start */}
} />
}>
@@ -331,8 +353,14 @@ function App(): JSX.Element {
{/* Additional Purchases */}
}>
} />
- } />
- } />
+ }
+ />
+ }
+ />
{/* Additional Purchases End */}
@@ -437,6 +465,10 @@ function App(): JSX.Element {
path={routes.client.wallpaper()}
element={}
/>
+ } />
+
+ } />
+
}
@@ -472,15 +504,9 @@ function App(): JSX.Element {
- }
- />
+ } />
- }
- />
+ } />
} />
@@ -568,6 +594,13 @@ function Layout({ setIsSpecialOfferOpen }: LayoutProps): JSX.Element {
image: "Compatibility.svg",
onClick: handleCompatibility,
},
+ {
+ title: "Advisors",
+ path: routes.client.advisors(),
+ paths: [routes.client.advisors()],
+ image: "moon.svg",
+ onClick: () => null,
+ },
{
title: "My Moon",
path: routes.client.wallpaper(),
diff --git a/src/components/BirthdayPage/index.tsx b/src/components/BirthdayPage/index.tsx
index 60278b6..fabfee9 100644
--- a/src/components/BirthdayPage/index.tsx
+++ b/src/components/BirthdayPage/index.tsx
@@ -1,54 +1,83 @@
-import { useState } from 'react'
-import { useNavigate } from 'react-router-dom'
-import { useTranslation } from 'react-i18next'
-import { useDispatch, useSelector } from 'react-redux'
-import { actions, selectors } from '@/store'
-import { DatePicker } from '../DateTimePicker'
-import MainButton from '../MainButton'
-import Policy from '../Policy'
-import Purposes from '../Purposes'
-import Title from '../Title'
-import routes from '@/routes'
-import './styles.css'
+import { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import { useDispatch, useSelector } from "react-redux";
+import { actions, selectors } from "@/store";
+import { DatePicker } from "../DateTimePicker";
+import MainButton from "../MainButton";
+import Policy from "../Policy";
+import Purposes from "../Purposes";
+import Title from "../Title";
+import routes from "@/routes";
+import "./styles.css";
function BirthdayPage(): JSX.Element {
- const { t } = useTranslation()
- const dispatch = useDispatch()
- const navigate = useNavigate()
- const birthdate = useSelector(selectors.selectBirthdate)
- const [isDisabled, setIsDisabled] = useState(true)
- const handleNext = () => navigate(routes.client.didYouKnow())
+ const { t } = useTranslation();
+ const dispatch = useDispatch();
+ 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();
+ const handleNext = () => navigate(nextRoute);
const handleValid = (birthdate: string) => {
- dispatch(actions.form.addDate(birthdate))
- setIsDisabled(birthdate === '')
- }
+ dispatch(actions.form.addDate(birthdate));
+ setIsDisabled(birthdate === "");
+ };
return (
-
- {t('lets_start')}
- {t('date_of_birth')}
+
+
+ {t("lets_start")}
+
+ {t("date_of_birth")}
setIsDisabled(true)}
- inputClassName='date-picker-input'
+ inputClassName="date-picker-input"
/>
- {t('next')}
+ {t("next")}
-
- )
+ );
}
-export default BirthdayPage
+export default BirthdayPage;
diff --git a/src/components/BreathPage/index.tsx b/src/components/BreathPage/index.tsx
index ca23057..d18d01f 100644
--- a/src/components/BreathPage/index.tsx
+++ b/src/components/BreathPage/index.tsx
@@ -29,6 +29,15 @@ function BreathPage({ leoApng }: BreathPageProps): JSX.Element {
const navigate = useNavigate();
const homeConfig = useSelector(selectors.selectHome);
const showNavbarFooter = homeConfig.isShowNavbar;
+ const queryTimeOutRef = useRef();
+
+ useEffect(() => {
+ return () => {
+ if (queryTimeOutRef.current) {
+ clearTimeout(queryTimeOutRef.current);
+ }
+ };
+ }, []);
useEffect(() => {
if (isOpenModal) return;
@@ -67,8 +76,7 @@ function BreathPage({ leoApng }: BreathPageProps): JSX.Element {
setIsOpenModal(false);
};
-
- const token = useSelector(selectors.selectToken)
+ const token = useSelector(selectors.selectToken);
const createCallback = useCallback(async () => {
const data: UserCallbacks.PayloadPost = {
data: {
@@ -87,7 +95,7 @@ function BreathPage({ leoApng }: BreathPageProps): JSX.Element {
token,
});
if (!getCallback.user_callback.is_complete) {
- setTimeout(getUserCallbacksRequest, 3000);
+ queryTimeOutRef.current = setTimeout(getUserCallbacksRequest, 3000);
}
dispatch(
actions.userCallbacks.update({
diff --git a/src/components/CompatResultPage/index.tsx b/src/components/CompatResultPage/index.tsx
index abe7088..5bd1588 100644
--- a/src/components/CompatResultPage/index.tsx
+++ b/src/components/CompatResultPage/index.tsx
@@ -3,7 +3,7 @@ import Title from "../Title";
import styles from "./styles.module.css";
import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store";
-import { useCallback, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
import { AICompats, AIRequests, useApi, useApiCall } from "@/api";
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
@@ -26,6 +26,15 @@ function CompatResultPage(): JSX.Element {
const [isOpenModal, setIsOpenModal] = useState(true);
const [isVisualLoading, setIsVisualLoading] = useState(true);
const [isDataLoading, setIsDataLoading] = useState(true);
+ const queryTimeOutRef = useRef();
+
+ useEffect(() => {
+ return () => {
+ if (queryTimeOutRef.current) {
+ clearTimeout(queryTimeOutRef.current);
+ }
+ };
+ }, []);
const handleNext = () => {
if (homeConfig.pathFromHome === EPathsFromHome.breath) {
@@ -67,7 +76,7 @@ function CompatResultPage(): JSX.Element {
token,
});
if (aIRequest.ai_request.state !== "ready") {
- setTimeout(loadAIRequest, 3000);
+ queryTimeOutRef.current = setTimeout(loadAIRequest, 3000);
}
setText(aIRequest?.ai_request?.response?.body || "Loading...");
setIsDataLoading(false);
diff --git a/src/components/Loader/index.tsx b/src/components/Loader/index.tsx
index 034bbaa..bf10f33 100644
--- a/src/components/Loader/index.tsx
+++ b/src/components/Loader/index.tsx
@@ -1,21 +1,30 @@
-import './styles.css'
+import "./styles.css";
export enum LoaderColor {
- White = 'white',
- Black = 'black',
+ White = "white",
+ Black = "black",
+ Red = "red",
}
type LoaderProps = {
- color?: LoaderColor
-}
+ color?: LoaderColor;
+ className?: string;
+};
-function Loader({ color = LoaderColor.Black }: LoaderProps): JSX.Element {
- const colorClass = color === LoaderColor.White ? 'loader__white' : 'loader__black'
+const colorClasses = {
+ [LoaderColor.White]: "loader__white",
+ [LoaderColor.Black]: "loader__black",
+ [LoaderColor.Red]: "loader__red",
+};
+
+function Loader({ color = LoaderColor.Black, className }: LoaderProps): JSX.Element {
return (
-
-
+
- )
+ );
}
-export default Loader
+export default Loader;
diff --git a/src/components/Loader/styles.css b/src/components/Loader/styles.css
index 476e0a7..7b33eb6 100644
--- a/src/components/Loader/styles.css
+++ b/src/components/Loader/styles.css
@@ -26,6 +26,10 @@
border-color: #fff;
}
+.loader.loader__red span::after {
+ border-color: #ff2c57;
+}
+
@keyframes loader-1-1 {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
diff --git a/src/components/NavbarFooter/index.tsx b/src/components/NavbarFooter/index.tsx
index 95c65dc..974c84e 100644
--- a/src/components/NavbarFooter/index.tsx
+++ b/src/components/NavbarFooter/index.tsx
@@ -29,7 +29,7 @@ function NavbarFooter({ items }: INavbarHomeProps): JSX.Element {
const [isShowOnboardingNavbarFooter, setIsShowOnboardingNavbarFooter] =
useState(!onboardingConfigNavbarFooter?.isShown);
- const [selectedOnboarding, setSelectedOnboarding] = useState(3);
+ const [selectedOnboarding, setSelectedOnboarding] = useState(4);
const [rerender, setRerender] = useState(false);
rerender
@@ -66,6 +66,12 @@ function NavbarFooter({ items }: INavbarHomeProps): JSX.Element {
onClick: () => setShownOnboarding(),
classNameText: `${styles["compatibility-onboarding__text"]} ${styles.onboarding}`,
},
+ // {
+ // text: t("au.web_onbording.moon"),
+ // onClick: () => setSelectedOnboarding(0),
+ // classNameText: `${styles["moon-onboarding__text"]} ${styles.onboarding}`,
+ // },
+ null,
{
text: t("au.web_onbording.moon"),
onClick: () => setSelectedOnboarding(0),
@@ -74,7 +80,7 @@ function NavbarFooter({ items }: INavbarHomeProps): JSX.Element {
];
const handleClick = (item: INavbarHomeItems) => {
- onboardingsSettings[selectedOnboarding].onClick();
+ onboardingsSettings[selectedOnboarding]?.onClick();
if (item.onClick) {
item.onClick();
}
@@ -96,9 +102,9 @@ function NavbarFooter({ items }: INavbarHomeProps): JSX.Element {
isShow={index === selectedOnboarding}
>
onboardingsSettings[index].onClick()}
- classNameText={onboardingsSettings[index].classNameText}
+ text={onboardingsSettings[index]?.text || ""}
+ crossClickHandler={() => onboardingsSettings[index]?.onClick()}
+ classNameText={onboardingsSettings[index]?.classNameText}
/>
)}
diff --git a/src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx b/src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx
index 59340d1..0cc4cd5 100644
--- a/src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx
+++ b/src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx
@@ -15,11 +15,13 @@ import styles from "./styles.module.css";
interface ICheckoutFormProps {
children?: JSX.Element | null;
subscriptionReceiptId?: string;
+ returnUrl?: string;
}
export default function CheckoutForm({
children,
subscriptionReceiptId,
+ returnUrl,
}: ICheckoutFormProps) {
const stripe = useStripe();
const elements = useElements();
@@ -43,7 +45,9 @@ export default function CheckoutForm({
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
- return_url: `https://${window.location.host}/payment/result/${subscriptionReceiptId}/`,
+ return_url: returnUrl
+ ? returnUrl
+ : `https://${window.location.host}/payment/result/${subscriptionReceiptId}/`,
},
});
if (error) {
@@ -67,7 +71,10 @@ export default function CheckoutForm({
onSubmit={handleSubmit}
>
{children ? children : null}
- setFormReady(true)} />
+ setFormReady(true)}
+ />
-
- {isProcessing ? "Processing..." : "Start"}
-
+ {isProcessing ? "Processing..." : "Start"}
{!!message.length && (
diff --git a/src/components/PaymentPage/results/index.tsx b/src/components/PaymentPage/results/index.tsx
index e0b3421..df19607 100644
--- a/src/components/PaymentPage/results/index.tsx
+++ b/src/components/PaymentPage/results/index.tsx
@@ -16,6 +16,7 @@ function PaymentResultPage(): JSX.Element {
const dispatch = useDispatch();
const [searchParams] = useSearchParams();
const status = searchParams.get("redirect_status");
+ const type = searchParams.get("type");
// const { id } = useParams();
// const requestTimeOutRef = useRef();
const [isLoading] = useState(true);
@@ -89,9 +90,17 @@ function PaymentResultPage(): JSX.Element {
useEffect(() => {
if (status === "succeeded") {
dispatch(actions.status.update("subscribed"));
- return navigate(routes.client.paymentSuccess());
+ let successPaymentRoute = routes.client.paymentSuccess();
+ if (type === "epe") {
+ successPaymentRoute = routes.client.epeSuccessPayment();
+ }
+ return navigate(successPaymentRoute);
}
- return navigate(routes.client.paymentFail());
+ let failPaymentRoute = routes.client.paymentFail();
+ if (type === "epe") {
+ failPaymentRoute = routes.client.epeFailPayment();
+ }
+ return navigate(failPaymentRoute);
}, [navigate, status, dispatch]);
return {isLoading && }
;
diff --git a/src/components/StripePage/ApplePayButton/index.tsx b/src/components/StripePage/ApplePayButton/index.tsx
index 201ec02..82ba844 100644
--- a/src/components/StripePage/ApplePayButton/index.tsx
+++ b/src/components/StripePage/ApplePayButton/index.tsx
@@ -15,12 +15,14 @@ interface ApplePayButtonProps {
activeSubPlan: ISubscriptionPlan | null;
client_secret: string;
subscriptionReceiptId?: string;
+ returnUrl?: string;
}
function ApplePayButton({
activeSubPlan,
client_secret,
subscriptionReceiptId,
+ returnUrl,
}: ApplePayButtonProps) {
const stripe = useStripe();
const elements = useElements();
@@ -78,7 +80,7 @@ function ApplePayButton({
return e.complete("fail");
}
navigate(
- `${routes.client.paymentResult()}/${subscriptionReceiptId}/?redirect_status=succeeded`
+ returnUrl || `${routes.client.paymentResult()}/${subscriptionReceiptId}/?redirect_status=succeeded`
);
e.complete("success");
// Show a success message to your customer
diff --git a/src/components/UserCallbacksPage/styles.module.css b/src/components/UserCallbacksPage/styles.module.css
index 73abc19..1bc8293 100644
--- a/src/components/UserCallbacksPage/styles.module.css
+++ b/src/components/UserCallbacksPage/styles.module.css
@@ -26,6 +26,7 @@
.result-container__value-label {
color: #fff;
font-weight: 500;
+ text-align: left;
}
.result-container__value-value {
diff --git a/src/components/pages/AdvisorChat/components/ChatHeader/index.tsx b/src/components/pages/AdvisorChat/components/ChatHeader/index.tsx
new file mode 100644
index 0000000..f9d31b7
--- /dev/null
+++ b/src/components/pages/AdvisorChat/components/ChatHeader/index.tsx
@@ -0,0 +1,30 @@
+import styles from "./styles.module.css";
+
+interface IChatHeaderProps {
+ name: string;
+ avatar: string;
+ classNameContainer?: string;
+ clickBackButton: () => void;
+}
+
+function ChatHeader({
+ name,
+ avatar,
+ classNameContainer = "",
+ clickBackButton,
+}: IChatHeaderProps) {
+ return (
+
+
+
+ {name}
+
+
+

+
+ );
+}
+
+export default ChatHeader;
diff --git a/src/components/pages/AdvisorChat/components/ChatHeader/styles.module.css b/src/components/pages/AdvisorChat/components/ChatHeader/styles.module.css
new file mode 100644
index 0000000..7097cf2
--- /dev/null
+++ b/src/components/pages/AdvisorChat/components/ChatHeader/styles.module.css
@@ -0,0 +1,71 @@
+.container {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ background: rgb(255 255 255 / 10%);
+ -webkit-backdrop-filter: blur(10px);
+ backdrop-filter: blur(10px);
+ padding: 2px 12px;
+}
+
+.avatar {
+ width: 42px;
+ height: 42px;
+ border-radius: 100%;
+}
+
+.online-status {
+ display: block;
+ width: 9px;
+ height: 9px;
+ background-color: rgb(6, 183, 6);
+ border-radius: 50%;
+ margin-top: 4px;
+}
+
+.name {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-weight: 700;
+}
+
+.back-button {
+ font-weight: 500;
+ color: rgb(0, 128, 255);
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 6px;
+ cursor: pointer;
+}
+
+.back-button > .arrow {
+ display: flex;
+ width: 14px;
+ height: 20px;
+ position: relative;
+}
+
+.back-button > .arrow::before,
+.back-button > .arrow::after {
+ content: "";
+ display: block;
+ width: 12px;
+ height: 2px;
+ background-color: rgb(0, 128, 255);
+ position: absolute;
+ left: 0;
+}
+
+.back-button > .arrow::before {
+ transform: rotate(-45deg);
+ top: calc(50% - 4px);
+}
+
+.back-button > .arrow::after {
+ transform: rotate(45deg);
+ top: calc(50% + 4px);
+}
diff --git a/src/components/pages/AdvisorChat/components/InputMessage/index.tsx b/src/components/pages/AdvisorChat/components/InputMessage/index.tsx
new file mode 100644
index 0000000..4aaed23
--- /dev/null
+++ b/src/components/pages/AdvisorChat/components/InputMessage/index.tsx
@@ -0,0 +1,56 @@
+import { ChangeEvent, FormEvent } from "react";
+import styles from "./styles.module.css";
+import Loader, { LoaderColor } from "@/components/Loader";
+
+interface IInputMessageProps {
+ placeholder?: string;
+ classNameContainer?: string;
+ messageText: string;
+ textareaRows: number;
+ disabledButton: boolean;
+ disabledTextArea: boolean;
+ isLoading: boolean;
+ handleChangeMessageText: (e: ChangeEvent) => void;
+ submitForm: (messageText: string) => void;
+}
+
+function InputMessage({
+ placeholder,
+ messageText,
+ textareaRows,
+ disabledButton,
+ disabledTextArea,
+ isLoading,
+ handleChangeMessageText,
+ submitForm,
+ classNameContainer = "",
+}: IInputMessageProps) {
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault();
+ submitForm(messageText);
+ };
+
+ return (
+
+ );
+}
+
+export default InputMessage;
diff --git a/src/components/pages/AdvisorChat/components/InputMessage/styles.module.css b/src/components/pages/AdvisorChat/components/InputMessage/styles.module.css
new file mode 100644
index 0000000..2283dee
--- /dev/null
+++ b/src/components/pages/AdvisorChat/components/InputMessage/styles.module.css
@@ -0,0 +1,72 @@
+.container {
+ display: grid;
+ grid-template-columns: 1fr 36px;
+ align-items: center;
+ width: 100%;
+ gap: 8px;
+ padding: 8px 12px;
+}
+
+.button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 36px;
+ width: 36px;
+ min-width: 0;
+ /* aspect-ratio: 1 / 1; */
+ background-color: rgb(0, 110, 255);
+ border: none;
+ border-radius: 100%;
+}
+
+button:disabled {
+ opacity: 0.5;
+}
+
+.button > .arrow {
+ display: block;
+ position: absolute;
+ width: 2px;
+ height: 10px;
+ background-color: #fff;
+}
+
+.button > .arrow::before,
+.button > .arrow::after {
+ content: "";
+ display: block;
+ width: 6px;
+ height: 2px;
+ background-color: #fff;
+ position: absolute;
+ top: 0;
+}
+
+.button > .arrow::before {
+ transform: rotate(-45deg);
+ left: -4px;
+}
+
+.button > .arrow::after {
+ transform: rotate(45deg);
+}
+
+.input {
+ border: solid 1px #929292;
+ outline: none;
+ background-color: #232322;
+ color: #fff;
+ font-size: 18px;
+ padding: 4px 14px;
+ border-radius: 16px;
+ height: inherit;
+ min-height: 34px;
+ /* max-height: 176px; */
+ /* width: calc(100% - 34px); */
+ resize: none;
+}
+
+.input:disabled {
+ opacity: 0.5;
+}
diff --git a/src/components/pages/AdvisorChat/components/LoaderDots/index.tsx b/src/components/pages/AdvisorChat/components/LoaderDots/index.tsx
new file mode 100644
index 0000000..06f4d23
--- /dev/null
+++ b/src/components/pages/AdvisorChat/components/LoaderDots/index.tsx
@@ -0,0 +1,9 @@
+import styles from "./styles.module.css"
+
+function LoaderDots() {
+ return (
+
+ )
+}
+
+export default LoaderDots
\ No newline at end of file
diff --git a/src/components/pages/AdvisorChat/components/LoaderDots/styles.module.css b/src/components/pages/AdvisorChat/components/LoaderDots/styles.module.css
new file mode 100644
index 0000000..14f904b
--- /dev/null
+++ b/src/components/pages/AdvisorChat/components/LoaderDots/styles.module.css
@@ -0,0 +1,72 @@
+.container {
+ position: relative;
+ left: -9999px;
+ width: 10px;
+ height: 10px;
+ border-radius: 5px;
+ background-color: #7f7f7f;
+ color: #7f7f7f;
+ box-shadow: 9999px 0 0 -5px;
+ animation: container 1.5s infinite linear;
+ animation-delay: 0.25s;
+ margin: 2px 24px;
+}
+.container::before,
+.container::after {
+ content: "";
+ display: inline-block;
+ position: absolute;
+ top: 0;
+ width: 10px;
+ height: 10px;
+ border-radius: 5px;
+ background-color: #7f7f7f;
+ color: #7f7f7f;
+}
+.container::before {
+ box-shadow: 9984px 0 0 -5px;
+ animation: container-before 1.5s infinite linear;
+ animation-delay: 0s;
+}
+.container::after {
+ box-shadow: 10014px 0 0 -5px;
+ animation: container-after 1.5s infinite linear;
+ animation-delay: 0.5s;
+}
+
+@keyframes container-before {
+ 0% {
+ box-shadow: 9984px 0 0 -5px;
+ }
+ 30% {
+ box-shadow: 9984px 0 0 2px;
+ }
+ 60%,
+ 100% {
+ box-shadow: 9984px 0 0 -5px;
+ }
+}
+@keyframes container {
+ 0% {
+ box-shadow: 9999px 0 0 -5px;
+ }
+ 30% {
+ box-shadow: 9999px 0 0 2px;
+ }
+ 60%,
+ 100% {
+ box-shadow: 9999px 0 0 -5px;
+ }
+}
+@keyframes container-after {
+ 0% {
+ box-shadow: 10014px 0 0 -5px;
+ }
+ 30% {
+ box-shadow: 10014px 0 0 2px;
+ }
+ 60%,
+ 100% {
+ box-shadow: 10014px 0 0 -5px;
+ }
+}
diff --git a/src/components/pages/AdvisorChat/components/Message/index.tsx b/src/components/pages/AdvisorChat/components/Message/index.tsx
new file mode 100644
index 0000000..d7055e6
--- /dev/null
+++ b/src/components/pages/AdvisorChat/components/Message/index.tsx
@@ -0,0 +1,45 @@
+import React from "react";
+import styles from "./styles.module.css";
+
+interface IMessageProps {
+ text: string | React.ReactNode;
+ isSelf: boolean;
+ advisorName?: string;
+ avatar?: string;
+ backgroundTextColor?: string;
+ textColor?: string;
+}
+
+// eslint-disable-next-line react-refresh/only-export-components
+function Message({
+ text,
+ avatar,
+ isSelf,
+ advisorName = "Advisor",
+ backgroundTextColor = "#0080ff",
+ textColor = "#fff",
+}: IMessageProps) {
+ return (
+
+ {!isSelf && avatar?.length && (
+

+ )}
+
+ {text}
+
+
+ );
+}
+
+// eslint-disable-next-line react-refresh/only-export-components
+export default React.memo(Message);
diff --git a/src/components/pages/AdvisorChat/components/Message/styles.module.css b/src/components/pages/AdvisorChat/components/Message/styles.module.css
new file mode 100644
index 0000000..3712b4c
--- /dev/null
+++ b/src/components/pages/AdvisorChat/components/Message/styles.module.css
@@ -0,0 +1,21 @@
+.container {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: flex-end;
+ gap: 8px;
+}
+
+.avatar {
+ width: 36px;
+ height: 36px;
+ border-radius: 100%;
+ object-fit: contain;
+}
+
+.text {
+ width: fit-content;
+ max-width: 70%;
+ padding: 8px;
+ border-radius: 8px;
+}
diff --git a/src/components/pages/AdvisorChat/index.tsx b/src/components/pages/AdvisorChat/index.tsx
new file mode 100644
index 0000000..297ce63
--- /dev/null
+++ b/src/components/pages/AdvisorChat/index.tsx
@@ -0,0 +1,362 @@
+import { useNavigate, useParams } from "react-router-dom";
+import styles from "./styles.module.css";
+import { ChangeEvent, useCallback, useEffect, useRef, useState } from "react";
+import { Assistants, useApi, useApiCall } from "@/api";
+import { useSelector } from "react-redux";
+import { selectors } from "@/store";
+import { IAssistant } from "@/api/resources/Assistants";
+import Loader, { LoaderColor } from "@/components/Loader";
+import ChatHeader from "./components/ChatHeader";
+import InputMessage from "./components/InputMessage";
+import routes from "@/routes";
+import { IMessage, ResponseRunThread } from "@/api/resources/OpenAI";
+import Message from "./components/Message";
+import LoaderDots from "./components/LoaderDots";
+import useDetectScroll from "@smakss/react-scroll-direction";
+
+function AdvisorChatPage() {
+ const { id } = useParams();
+ const api = useApi();
+ const navigate = useNavigate();
+ const { scrollDir, scrollPosition } = useDetectScroll();
+ const token = useSelector(selectors.selectToken);
+ const openAiToken = useSelector(selectors.selectOpenAiToken);
+ const [assistant, setAssistant] = useState();
+ const [messageText, setMessageText] = useState("");
+ const [textareaRows, setTextareaRows] = useState(1);
+ const [messages, setMessages] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isLoadingSelfMessage, setIsLoadingSelfMessage] = useState(false);
+ const [isLoadingAdvisorMessage, setIsLoadingAdvisorMessage] = useState(false);
+ const [isLoadingLatestMessages, setIsLoadingLatestMessages] = useState(false);
+ const timeOutStatusRunRef = useRef();
+ const messagesEndRef = useRef(null);
+ const [hasMoreLatestMessages, setHasMoreLatestMessages] = useState(true);
+
+ useEffect(() => {
+ if (
+ scrollDir === "up" &&
+ scrollPosition.top < 50 &&
+ !isLoadingLatestMessages
+ ) {
+ loadLatestMessages();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [scrollDir, scrollPosition]);
+
+ const loadLatestMessages = async () => {
+ const lastIdMessage = messages[messages.length - 1]?.id;
+ if (!hasMoreLatestMessages || !lastIdMessage) {
+ return;
+ }
+ setIsLoadingLatestMessages(true);
+ const listMessages = await api.getListMessages({
+ token: openAiToken,
+ method: "GET",
+ path: routes.openAi.getListMessages(assistant?.external_chat_id || ""),
+ QueryParams: {
+ after: lastIdMessage,
+ limit: 20,
+ },
+ });
+ setHasMoreLatestMessages(listMessages.has_more);
+ setMessages((prev) => [...prev, ...listMessages.data]);
+ setIsLoadingLatestMessages(false);
+ };
+
+ const scrollToBottom = () => {
+ setTimeout(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+ }, 100);
+ };
+
+ useEffect(() => {
+ return () => {
+ if (timeOutStatusRunRef.current) {
+ clearTimeout(timeOutStatusRunRef.current);
+ }
+ };
+ }, []);
+
+ const getCurrentAssistant = (
+ aiAssistants: Assistants.IAssistant[],
+ idAssistant: string | number
+ ) => {
+ const currentAssistant = aiAssistants.find(
+ (a) => a.id === Number(idAssistant)
+ );
+ return currentAssistant;
+ };
+
+ const updateMessages = async (threadId: string) => {
+ const listMessages = await api.getListMessages({
+ token: openAiToken,
+ method: "GET",
+ path: routes.openAi.getListMessages(threadId),
+ });
+ setMessages(listMessages.data);
+ scrollToBottom();
+ };
+
+ const setExternalChatIdAssistant = async (threadId: string) => {
+ await api.setExternalChatIdAssistant({
+ token,
+ chatId: String(id),
+ ai_assistant_chat: {
+ external_id: threadId,
+ },
+ });
+ };
+
+ const updateCurrentAssistant = async () => {
+ const { ai_assistants } = await api.assistants({
+ token,
+ });
+ const currentAssistant = getCurrentAssistant(ai_assistants, id || "");
+ setAssistant(currentAssistant);
+ return {
+ ai_assistants,
+ currentAssistant,
+ };
+ };
+
+ const loadData = useCallback(async () => {
+ const { ai_assistants, currentAssistant } = await updateCurrentAssistant();
+ let listRuns: ResponseRunThread[] = [];
+ if (currentAssistant?.external_chat_id?.length) {
+ await updateMessages(currentAssistant.external_chat_id);
+ const result = await api.getListRuns({
+ token: openAiToken,
+ method: "GET",
+ path: routes.openAi.getListRuns(currentAssistant.external_chat_id),
+ });
+ listRuns = result.data;
+ }
+
+ setIsLoading(false);
+ const runInProgress = listRuns.find((r) => r.status === "in_progress");
+
+ setTimeout(() => {
+ scrollToBottom();
+ });
+
+ if (runInProgress) {
+ setIsLoadingAdvisorMessage(true);
+
+ await checkStatusAndGetLastMessage(
+ runInProgress.thread_id,
+ runInProgress.id
+ );
+ setIsLoadingAdvisorMessage(false);
+ }
+ return { ai_assistants };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [api, id, openAiToken, token]);
+
+ useApiCall(loadData);
+
+ const createThread = async (messageText: string) => {
+ const thread = await api.createThread({
+ token: openAiToken,
+ method: "POST",
+ path: routes.openAi.createThread(),
+ messages: [
+ {
+ role: "user",
+ content: messageText,
+ },
+ ],
+ });
+ await setExternalChatIdAssistant(thread.id);
+ await updateCurrentAssistant();
+ return thread;
+ };
+
+ const getStatusThreadRun = async (
+ threadId: string,
+ runId: string
+ ): Promise => {
+ await new Promise(
+ (resolve) => (timeOutStatusRunRef.current = setTimeout(resolve, 1500))
+ );
+ const run = await api.getStatusRunThread({
+ token: openAiToken,
+ method: "GET",
+ path: routes.openAi.getStatusRunThread(threadId, runId),
+ assistant_id: `${assistant?.external_id}`,
+ });
+ if (run.status !== "completed") {
+ return await getStatusThreadRun(threadId, runId);
+ }
+ return run;
+ };
+
+ const createMessage = async (messageText: string, threadId: string) => {
+ const message = await api.createMessage({
+ token: openAiToken,
+ method: "POST",
+ path: routes.openAi.createMessage(threadId),
+ role: "user",
+ content: messageText,
+ });
+ return message;
+ };
+
+ const runThread = async (threadId: string, assistantId: string) => {
+ const run = await api.runThread({
+ token: openAiToken,
+ method: "POST",
+ path: routes.openAi.runThread(threadId),
+ assistant_id: assistantId,
+ });
+ return run;
+ };
+
+ const checkStatusAndGetLastMessage = async (
+ threadId: string,
+ runId: string
+ ) => {
+ const { status } = await getStatusThreadRun(threadId, runId);
+
+ if (status === "completed") {
+ await getLastMessage(threadId);
+ }
+ };
+
+ const getLastMessage = async (threadId: string) => {
+ const lastMessage = await api.getListMessages({
+ token: openAiToken,
+ method: "GET",
+ path: routes.openAi.getListMessages(threadId),
+ QueryParams: {
+ limit: 1,
+ },
+ });
+ setMessages((prev) => [lastMessage.data[0], ...prev]);
+ };
+
+ const sendMessage = async (messageText: string) => {
+ setMessageText("");
+ setIsLoadingSelfMessage(true);
+ let threadId = "";
+ let assistantId = "";
+
+ if (assistant?.external_chat_id?.length) {
+ const message = await createMessage(
+ messageText,
+ assistant.external_chat_id
+ );
+ setMessages((prev) => [message, ...prev]);
+ setIsLoadingSelfMessage(false);
+ setIsLoadingAdvisorMessage(true);
+ threadId = assistant.external_chat_id;
+ assistantId = assistant.external_id || "";
+ } else {
+ const thread = await createThread(messageText);
+ threadId = thread.id;
+ assistantId = assistant?.external_id || "";
+
+ await getLastMessage(threadId);
+ setIsLoadingSelfMessage(false);
+ setIsLoadingAdvisorMessage(true);
+ }
+ setTimeout(() => {
+ scrollToBottom();
+ });
+
+ const run = await runThread(threadId, assistantId);
+
+ await checkStatusAndGetLastMessage(threadId, run.id);
+ setTimeout(() => {
+ scrollToBottom();
+ });
+ setIsLoadingAdvisorMessage(false);
+ };
+
+ const handleChangeMessageText = (e: ChangeEvent) => {
+ const { scrollHeight, clientHeight, value } = e.target;
+ if (
+ scrollHeight > clientHeight &&
+ textareaRows < 5 &&
+ value.length > messageText.length
+ ) {
+ setTextareaRows((prev) => prev + 1);
+ }
+ if (
+ scrollHeight === clientHeight &&
+ textareaRows > 1 &&
+ value.length < messageText.length
+ ) {
+ setTextareaRows((prev) => prev - 1);
+ }
+ setMessageText(e.target.value);
+ };
+
+ const getIsSelfMessage = (role: string) => role === "user";
+
+ return (
+
+ {isLoading && (
+
+ )}
+ {!isLoading && (
+ navigate(-1)}
+ />
+ )}
+ {!!messages.length && (
+
+ {isLoadingAdvisorMessage && (
+
}
+ isSelf={false}
+ backgroundTextColor={"#c9c9c9"}
+ textColor={"#000"}
+ />
+ )}
+ {messages.map((message) =>
+ message.content.map((content) => (
+
+ ))
+ )}
+
+ {isLoadingLatestMessages && }
+
+
+ )}
+
+
+ {!isLoading && (
+
+ )}
+
+ );
+}
+
+export default AdvisorChatPage;
diff --git a/src/components/pages/AdvisorChat/styles.module.css b/src/components/pages/AdvisorChat/styles.module.css
new file mode 100644
index 0000000..b496526
--- /dev/null
+++ b/src/components/pages/AdvisorChat/styles.module.css
@@ -0,0 +1,52 @@
+.page {
+ position: relative;
+ height: fit-content;
+ min-height: 100dvh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-end;
+ background-color: #232322;
+ color: #fff;
+ padding: 64px 6px;
+}
+
+.header-container {
+ position: fixed;
+ top: 0;
+ left: 50%;
+ transform: translate(-50%, 0);
+ max-width: 560px;
+ z-index: 10;
+}
+
+.input-container {
+ position: fixed;
+ width: 100%;
+ bottom: 0;
+ left: 50%;
+ transform: translate(-50%, 0);
+ max-width: 560px;
+ z-index: 10;
+}
+
+.messages-container {
+ display: flex;
+ flex-direction: column-reverse;
+ gap: 16px;
+ width: 100%;
+}
+
+.loader-container {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.loader {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
diff --git a/src/components/pages/Advisors/components/AssistantCard/index.tsx b/src/components/pages/Advisors/components/AssistantCard/index.tsx
new file mode 100644
index 0000000..3c4f1a1
--- /dev/null
+++ b/src/components/pages/Advisors/components/AssistantCard/index.tsx
@@ -0,0 +1,47 @@
+import { IAssistant } from "@/api/resources/Assistants";
+import styles from "./styles.module.css";
+import MainButton from "@/components/MainButton";
+import Title from "@/components/Title";
+
+interface IAssistantCardProps extends IAssistant {
+ onClickCard: () => void;
+}
+
+function AssistantCard({
+ name,
+ expirience,
+ stars,
+ photo: { th2x },
+ rating,
+ onClickCard,
+}: IAssistantCardProps) {
+ return (
+
+
+
+ {name}
+
+
+
+
+
{expirience}
+
+
+ {Array(stars)
+ .fill(0)
+ .map((_, index) => (
+

+ ))}
+
| {rating}
+
+
+
CHAT | FREE
+
+
+ );
+}
+
+export default AssistantCard;
diff --git a/src/components/pages/Advisors/components/AssistantCard/styles.module.css b/src/components/pages/Advisors/components/AssistantCard/styles.module.css
new file mode 100644
index 0000000..427f8d4
--- /dev/null
+++ b/src/components/pages/Advisors/components/AssistantCard/styles.module.css
@@ -0,0 +1,79 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ height: fit-content;
+ border: solid #b3b3b3 1px;
+ border-radius: 12px;
+ background-color: #000;
+ justify-content: space-between;
+}
+
+.header {
+ height: 167px;
+ background-position: center;
+ background-size: cover;
+ background-repeat: no-repeat;
+ box-shadow: inset 0 -24px 22px #000;
+ border-top-left-radius: 12px;
+ border-top-right-radius: 12px;
+ display: flex;
+ flex-direction: column-reverse;
+ padding-left: 8px;
+}
+
+.name {
+ color: #fff;
+ width: 100%;
+ text-align: left;
+ font-weight: 700;
+ font-size: 18px;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.online-status {
+ display: block;
+ width: 9px;
+ height: 9px;
+ background-color: rgb(6, 183, 6);
+ border-radius: 50%;
+ margin-top: 4px;
+}
+
+.rating-container {
+ margin-top: 6px;
+ font-size: 12px;
+}
+
+.stars > img {
+ width: 12px;
+ margin-right: 4px;
+}
+
+.footer {
+ color: gray;
+ font-size: 14px;
+ font-weight: 500;
+ padding: 8px;
+}
+
+.button {
+ width: 100%;
+ height: 34px;
+ min-width: 0;
+ min-height: 0;
+ border-radius: 32px;
+ background-color: rgb(37, 107, 239);
+ font-size: 16px;
+ margin-top: 8px;
+}
+
+.expirience {
+ white-space: nowrap;
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: 100%;
+}
diff --git a/src/components/pages/Advisors/index.tsx b/src/components/pages/Advisors/index.tsx
new file mode 100644
index 0000000..aa4549b
--- /dev/null
+++ b/src/components/pages/Advisors/index.tsx
@@ -0,0 +1,60 @@
+import Title from "@/components/Title";
+import styles from "./styles.module.css";
+import { useCallback, useState } from "react";
+import { Assistants, useApi, useApiCall } from "@/api";
+import { useDispatch, useSelector } from "react-redux";
+import { selectors } from "@/store";
+import { IAssistant } from "@/api/resources/Assistants";
+import AssistantCard from "./components/AssistantCard";
+import Loader, { LoaderColor } from "@/components/Loader";
+import { useNavigate } from "react-router-dom";
+import routes from "@/routes";
+
+function Advisors() {
+ const api = useApi();
+ const dispatch = useDispatch();
+ const token = useSelector(selectors.selectToken);
+ const navigate = useNavigate();
+ const [assistants, setAssistants] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ const loadData = useCallback(async () => {
+ const { ai_assistants } = await api.assistants({
+ token,
+ });
+ setAssistants(ai_assistants);
+ setIsLoading(false);
+ return { ai_assistants };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [api, dispatch, token]);
+
+ useApiCall(loadData);
+
+ const handleAdvisorClick = (assistant: IAssistant) => {
+ navigate(routes.client.advisorChat(assistant.id));
+ };
+
+ return (
+
+
+ Advisors
+
+ {!!assistants?.length && !isLoading && (
+
+ {assistants.map((assistant, index) => (
+
handleAdvisorClick(assistant)}
+ />
+ ))}
+
+ )}
+ {isLoading && (
+
+ )}
+
+ );
+}
+
+export default Advisors;
diff --git a/src/components/pages/Advisors/styles.module.css b/src/components/pages/Advisors/styles.module.css
new file mode 100644
index 0000000..1549758
--- /dev/null
+++ b/src/components/pages/Advisors/styles.module.css
@@ -0,0 +1,28 @@
+.page {
+ position: relative;
+ height: fit-content;
+ min-height: 100dvh;
+ background-color: #232322;
+ padding-top: 32px;
+ padding-bottom: 116px;
+}
+
+.title {
+ width: 100%;
+ text-align: left;
+ color: #fff;
+}
+
+.loader {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.advisors-container {
+ width: 100%;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(145px, 1fr));
+ gap: 12px;
+}
diff --git a/src/components/pages/Gender/index.tsx b/src/components/pages/Gender/index.tsx
index 9c4cbba..554a250 100644
--- a/src/components/pages/Gender/index.tsx
+++ b/src/components/pages/Gender/index.tsx
@@ -1,6 +1,7 @@
import styles from "./styles.module.css";
import Title from "@/components/Title";
import { Gender, genders } from "@/data";
+import routes from "@/routes";
import { actions } from "@/store";
import { useEffect } from "react";
import { useDispatch } from "react-redux";
@@ -10,6 +11,7 @@ function GenderPage(): JSX.Element {
const dispatch = useDispatch();
const navigate = useNavigate();
const { targetId } = useParams();
+ const pathName = window.location.pathname;
useEffect(() => {
const isShowTryApp = targetId === "i";
@@ -18,6 +20,9 @@ function GenderPage(): JSX.Element {
const selectGender = (gender: Gender) => {
dispatch(actions.questionnaire.update({ gender: gender.id }));
+ if (pathName === "/epe/gender") {
+ return navigate(routes.client.epeBirthdate());
+ }
navigate(`/questionnaire/profile/flowChoice`);
};
diff --git a/src/components/pages/PaymentWithEmailPage/PaymentForm/index.tsx b/src/components/pages/PaymentWithEmailPage/PaymentForm/index.tsx
new file mode 100644
index 0000000..bb6a583
--- /dev/null
+++ b/src/components/pages/PaymentWithEmailPage/PaymentForm/index.tsx
@@ -0,0 +1,36 @@
+import { Elements } from "@stripe/react-stripe-js";
+import styles from "./styles.module.css";
+import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm";
+import { useEffect, useState } from "react";
+import { Stripe, loadStripe } from "@stripe/stripe-js";
+import SecurityPayments from "../../TrialPayment/components/SecurityPayments";
+
+interface IPaymentFormProps {
+ stripePublicKey: string;
+ clientSecret: string;
+ returnUrl: string;
+}
+
+function PaymentForm({ stripePublicKey, clientSecret, returnUrl }: IPaymentFormProps) {
+ const [stripePromise, setStripePromise] =
+ useState | null>(null);
+
+ useEffect(() => {
+ setStripePromise(loadStripe(stripePublicKey));
+ }, [stripePublicKey]);
+ return (
+
+
+ {stripePromise && clientSecret && (
+
+
+
+ )}
+
+
+
500 N RAINBOW BLVD LAS VEGAS, NV 89107
+
+ );
+}
+
+export default PaymentForm;
diff --git a/src/components/pages/PaymentWithEmailPage/PaymentForm/styles.module.css b/src/components/pages/PaymentWithEmailPage/PaymentForm/styles.module.css
new file mode 100644
index 0000000..ac07530
--- /dev/null
+++ b/src/components/pages/PaymentWithEmailPage/PaymentForm/styles.module.css
@@ -0,0 +1,44 @@
+.payment-modal {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 250px;
+ gap: 25px;
+ color: #2f2e37;
+}
+
+.title {
+ font-weight: 700;
+ font-size: 20px;
+ line-height: 20px;
+ text-align: center;
+ margin: 0;
+}
+
+.sub-plan-description {
+ font-size: 12px;
+ text-align: center;
+ line-height: 150%;
+}
+
+.payment-method-container {
+ width: 100%;
+}
+
+.address {
+ margin-bottom: 24px;
+}
+
+.payment-method {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 16px;
+}
+
+.address {
+ color: gray;
+ font-size: 10px;
+}
diff --git a/src/components/pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage/index.tsx b/src/components/pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage/index.tsx
new file mode 100644
index 0000000..0bb67d7
--- /dev/null
+++ b/src/components/pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage/index.tsx
@@ -0,0 +1,34 @@
+import { useNavigate } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import routes from "@/routes";
+import styles from "./styles.module.css";
+import Title from "@/components/Title";
+import MainButton from "@/components/MainButton";
+
+function FailPaymentPage(): JSX.Element {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const handleNext = () => navigate(routes.client.epePayment());
+
+ return (
+
+
+
+
{t("auweb.pay_bad.title")}
+
{t("auweb.pay_bad.text1")}
+
+
+
{t("auweb.pay_bad.text2")}
+
+ {t("auweb.pay_bad.button")}
+
+
+
+ );
+}
+
+export default FailPaymentPage;
diff --git a/src/components/pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage/styles.module.css b/src/components/pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage/styles.module.css
new file mode 100644
index 0000000..fa479f4
--- /dev/null
+++ b/src/components/pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage/styles.module.css
@@ -0,0 +1,38 @@
+.page {
+ position: relative;
+ height: calc(100vh - 50px);
+ /* max-height: -webkit-fill-available; */
+ overflow-y: scroll;
+ justify-content: center;
+ gap: 80px;
+}
+
+.text {
+ display: flex;
+ flex-direction: column;
+}
+
+.list {
+ font-weight: 500;
+ white-space: pre-wrap;
+ text-align: left;
+}
+
+.description {
+ font-weight: 500;
+ text-align: center;
+}
+
+.button {
+ width: 100%;
+ max-width: 260px;
+ border-radius: 50px;
+ background-color: #fe2b57;
+}
+
+.bottom {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+}
diff --git a/src/components/pages/PaymentWithEmailPage/ResultPayment/SuccessPaymentPage/index.tsx b/src/components/pages/PaymentWithEmailPage/ResultPayment/SuccessPaymentPage/index.tsx
new file mode 100644
index 0000000..219efb1
--- /dev/null
+++ b/src/components/pages/PaymentWithEmailPage/ResultPayment/SuccessPaymentPage/index.tsx
@@ -0,0 +1,23 @@
+import { useTranslation } from "react-i18next";
+import styles from "./styles.module.css";
+import Title from "@/components/Title";
+
+function SuccessPaymentPage(): JSX.Element {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
The information has been sent to your e-mail
+
{t("auweb.pay_good.text1")}
+
+
+ );
+}
+
+export default SuccessPaymentPage;
diff --git a/src/components/pages/PaymentWithEmailPage/ResultPayment/SuccessPaymentPage/styles.module.css b/src/components/pages/PaymentWithEmailPage/ResultPayment/SuccessPaymentPage/styles.module.css
new file mode 100644
index 0000000..88e4c06
--- /dev/null
+++ b/src/components/pages/PaymentWithEmailPage/ResultPayment/SuccessPaymentPage/styles.module.css
@@ -0,0 +1,25 @@
+.page {
+ position: relative;
+ flex: auto;
+ height: calc(100vh - 50px);
+ max-height: -webkit-fill-available;
+ justify-content: center;
+ gap: 80px;
+}
+
+.text {
+ display: flex;
+ flex-direction: column;
+}
+
+.text > p {
+ text-align: center;
+ font-weight: 500;
+}
+
+.button {
+ width: 100%;
+ max-width: 260px;
+ border-radius: 50px;
+ background-color: #fe2b57;
+}
diff --git a/src/components/pages/PaymentWithEmailPage/index.tsx b/src/components/pages/PaymentWithEmailPage/index.tsx
new file mode 100644
index 0000000..1dd5e80
--- /dev/null
+++ b/src/components/pages/PaymentWithEmailPage/index.tsx
@@ -0,0 +1,292 @@
+import EmailInput from "@/components/EmailEnterPage/EmailInput";
+import styles from "./styles.module.css";
+import { useCallback, useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { actions, selectors } from "@/store";
+import { useDispatch, useSelector } from "react-redux";
+import MainButton from "@/components/MainButton";
+import Loader, { LoaderColor } from "@/components/Loader";
+import { useAuth } from "@/auth";
+import { ApiError, extractErrorMessage, useApi } from "@/api";
+import { getClientTimezone } from "@/locales";
+import ErrorText from "@/components/ErrorText";
+import Title from "@/components/Title";
+import NameInput from "@/components/EmailEnterPage/NameInput";
+import { getZodiacSignByDate } from "@/services/zodiac-sign";
+import {
+ ResponseGet,
+ ResponsePost,
+ ResponsePostExistPaymentData,
+} from "@/api/resources/SinglePayment";
+import { useNavigate } from "react-router-dom";
+import routes from "@/routes";
+import PaymentForm from "./PaymentForm";
+import { getPriceCentsToDollars } from "@/services/price";
+import { User } from "@/api/resources/User";
+
+function PaymentWithEmailPage() {
+ 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 locale = i18n.language;
+ const [email, setEmail] = useState("");
+ const [name, setName] = useState("");
+ const [isValidEmail, setIsValidEmail] = useState(false);
+ const [isValidName, setIsValidName] = useState(true);
+ const [isDisabled, setIsDisabled] = useState(true);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isLoadingPage, setIsLoadingPage] = useState(false);
+ const [isAuth, setIsAuth] = useState(false);
+ const [apiError, setApiError] = useState(null);
+ const [error, setError] = useState(false);
+ const [paymentIntent, setPaymentIntent] = useState<
+ ResponsePost | ResponsePostExistPaymentData | null
+ >(null);
+ const [currentProduct, setCurrentProduct] = useState();
+ const returnUrl = `${window.location.protocol}//${window.location.host}/payment/result/?type=epe`;
+
+ useEffect(() => {
+ if (isValidName && isValidEmail) {
+ setIsDisabled(false);
+ } else {
+ setIsDisabled(true);
+ }
+ }, [isValidEmail, email, isValidName, name]);
+
+ const handleValidEmail = (email: string) => {
+ dispatch(actions.form.addEmail(email));
+ setEmail(email);
+ setIsValidEmail(true);
+ };
+
+ const handleValidName = (name: string) => {
+ setName(name);
+ setIsValidName(true);
+ };
+
+ const authorization = async () => {
+ try {
+ setIsLoading(true);
+ const auth = await api.auth({ email, timezone, locale });
+ const {
+ auth: { token, user },
+ } = auth;
+ signUp(token, user);
+ const payload = {
+ user: {
+ profile_attributes: {
+ birthday,
+ full_name: name,
+ },
+ },
+ token,
+ };
+ const updatedUser = await api.updateUser(payload).catch((error) => {
+ console.log("Error: ", error);
+ });
+
+ if (updatedUser?.user) {
+ dispatch(actions.user.update(updatedUser.user));
+ }
+ if (name) {
+ dispatch(
+ actions.user.update({
+ username: name,
+ })
+ );
+ }
+ dispatch(actions.status.update("registred"));
+ setIsAuth(true);
+ const userUpdated = await api.getUser({ token });
+ return { user: userUpdated?.user, token };
+ } catch (error) {
+ console.error(error);
+ if (error instanceof ApiError) {
+ setApiError(error as ApiError);
+ } else {
+ setError(true);
+ }
+ }
+ };
+
+ const getCurrentProduct = async (token: string) => {
+ const productsSinglePayment = await api.getSinglePaymentProducts({
+ token,
+ });
+ const currentProduct = productsSinglePayment.find(
+ (product) => product.key === "compatibility.pdf"
+ );
+ return currentProduct;
+ };
+
+ const createSinglePayment = async (
+ user: User,
+ productId: string,
+ token: string,
+ email: string,
+ name: string | null
+ ) => {
+ return await api.createSinglePayment({
+ token,
+ data: {
+ user: {
+ id: `${user?.id}`,
+ email,
+ name: name || "",
+ sign: user?.profile?.sign?.sign || getZodiacSignByDate(birthday),
+ age: user?.profile?.age?.years || 1,
+ },
+ partner: {
+ sign: "partner_cancer",
+ age: 26,
+ },
+ paymentInfo: {
+ productId,
+ },
+ return_url: returnUrl,
+ },
+ });
+ };
+
+ const handleClick = async () => {
+ const authData = await authorization();
+ if (!authData) {
+ return;
+ }
+ const { user, token } = authData;
+
+ const currentProduct = await getCurrentProduct(token);
+ if (!currentProduct) {
+ setError(true);
+ return;
+ }
+ setCurrentProduct(currentProduct);
+
+ const { productId } = currentProduct;
+ const paymentIntent = await createSinglePayment(
+ user,
+ productId,
+ token,
+ email,
+ name
+ );
+ 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 } = currentProduct;
+ const paymentIntent = await createSinglePayment(
+ userFromStore,
+ productId,
+ tokenFromStore,
+ userFromStore.email,
+ userFromStore.profile.full_name
+ );
+ 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
+ }, []);
+
+ useEffect(() => {
+ handleAuthUser();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+ {isLoadingPage &&
}
+ {!isLoadingPage &&
+ paymentIntent &&
+ "paymentIntent" in paymentIntent &&
+ !!tokenFromStore.length && (
+ <>
+
+ {getPriceCentsToDollars(currentProduct?.amount || 0)}$
+
+
+ >
+ )}
+ {(!tokenFromStore || !paymentIntent) && !isLoadingPage && (
+ <>
+
setIsValidName(true)}
+ />
+ setIsValidEmail(false)}
+ />
+
+
+ {isLoading && }
+ {!isLoading &&
+ !(!apiError && !error && !isLoading && isAuth) &&
+ t("_continue")}
+ {!apiError && !error && !isLoading && isAuth && (
+
+ )}
+
+ >
+ )}
+ {(error || apiError) && (
+
+ Something went wrong
+
+ )}
+ {apiError && (
+
+ )}
+
+ );
+}
+
+export default PaymentWithEmailPage;
diff --git a/src/components/pages/PaymentWithEmailPage/styles.module.css b/src/components/pages/PaymentWithEmailPage/styles.module.css
new file mode 100644
index 0000000..27971fd
--- /dev/null
+++ b/src/components/pages/PaymentWithEmailPage/styles.module.css
@@ -0,0 +1,61 @@
+.page {
+ /* position: relative; */
+ position: static;
+ height: fit-content;
+ min-height: calc(100dvh - 103px);
+ /* max-height: -webkit-fill-available; */
+ display: flex;
+ justify-items: center;
+ justify-content: center;
+ align-items: center;
+ /* gap: 16px; */
+}
+
+.button {
+ border-radius: 12px;
+ margin-top: 0;
+ box-shadow: rgba(0, 0, 0, 0.25) 0px 4px 4px 0px;
+ height: 50px;
+ min-height: 0;
+ 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%
+ );
+ font-size: 18px;
+ line-height: 21px;
+}
+
+.payment-loader {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.cross {
+ position: absolute;
+ top: -36px;
+ right: 28px;
+ width: 22px;
+ height: 22px;
+ cursor: pointer;
+ z-index: 9;
+}
+
+.title {
+ font-size: 27px;
+ font-weight: 700;
+ margin: 0;
+}
+
+.email {
+ font-size: 17px;
+ font-weight: 500;
+ margin: 0;
+}
+
+.success-icon {
+ height: 100%;
+}
diff --git a/src/components/pages/QuestionnaireIntermediate/index.tsx b/src/components/pages/QuestionnaireIntermediate/index.tsx
index 17686ad..ae114af 100644
--- a/src/components/pages/QuestionnaireIntermediate/index.tsx
+++ b/src/components/pages/QuestionnaireIntermediate/index.tsx
@@ -39,7 +39,13 @@ function QuestionnaireIntermediatePage() {
backgroundImage: `url(${backgroundImage})`,
}}
>
-
+
{path && (
diff --git a/src/components/pages/TrialPayment/components/PaymentModal/index.tsx b/src/components/pages/TrialPayment/components/PaymentModal/index.tsx
index 472ade4..43f70ef 100644
--- a/src/components/pages/TrialPayment/components/PaymentModal/index.tsx
+++ b/src/components/pages/TrialPayment/components/PaymentModal/index.tsx
@@ -22,9 +22,11 @@ import PayPalButton from "./components/PayPalButton";
interface IPaymentModalProps {
activeSubscriptionPlan?: ISubscriptionPlan;
+ noTrial?: boolean;
+ returnUrl?: string;
}
-function PaymentModal({ activeSubscriptionPlan }: IPaymentModalProps) {
+function PaymentModal({ activeSubscriptionPlan, noTrial, returnUrl }: IPaymentModalProps) {
const { i18n } = useTranslation();
const locale = i18n.language;
const api = useApi();
@@ -57,6 +59,9 @@ function PaymentModal({ activeSubscriptionPlan }: IPaymentModalProps) {
useEffect(() => {
(async () => {
const siteConfig = await api.getAppConfig({ bundleId: "auraweb" });
+ // const isProduction = import.meta.env.MODE === "production";
+ // const stripePublicKey = isProduction ? siteConfig.data.stripe_public_key : "pk_test_51Ndqf4IlX4lgwUxrlLWqfYWpo0Ic0BV7DfiZxfMYy838IZP8NLrwwZ5i0HhhbOQBGoQZe4Rrel1ziEk8mhQ2TE3500ETWZPBva";
+ // setStripePromise(loadStripe(stripePublicKey));
setStripePromise(loadStripe(siteConfig.data.stripe_public_key));
const { sub_plans } = await api.getSubscriptionPlans({ locale });
setSubPlans(sub_plans);
@@ -170,22 +175,27 @@ function PaymentModal({ activeSubscriptionPlan }: IPaymentModalProps) {
/>
{activeSubPlan && (
-
- You will be charged only{" "}
-
- ${getPriceFromTrial(activeSubPlan?.trial)} for your 3-day trial.
-
-
-
- We`ll email you a reminder before your trial period ends.
-
+ {!noTrial && (
+ <>
+
+ You will be charged only{" "}
+
+ ${getPriceFromTrial(activeSubPlan?.trial)} for your 3-day trial.
+
+
+
+ We`ll email you a reminder before your trial period ends.
+
+ >
+ )}
+
Cancel anytime. The charge will appear on your bill as witapps.
)}
- {stripePromise && clientSecret && subscriptionReceiptId && (
+ {stripePromise && clientSecret && (
{selectedPaymentMethod === EPaymentMethod.PAYPAL_OR_APPLE_PAY && (
@@ -199,13 +209,14 @@ function PaymentModal({ activeSubscriptionPlan }: IPaymentModalProps) {
activeSubPlan={activeSubPlan}
client_secret={clientSecret}
subscriptionReceiptId={subscriptionReceiptId}
+ returnUrl={window.location.href}
/>
{!!errors.length &&
{errors}
}
)}
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
-
+
)}
)}
diff --git a/src/components/palmistry/discount-screen/discount-screen.css b/src/components/palmistry/discount-screen/discount-screen.css
index e120a92..ffbf7c2 100644
--- a/src/components/palmistry/discount-screen/discount-screen.css
+++ b/src/components/palmistry/discount-screen/discount-screen.css
@@ -58,6 +58,11 @@
border: 2px solid #c7c7c7;
}
+.discount-screen__block:first-child .discount-screen__button {
+ background: #c7c7c7;
+ color: #000;
+}
+
.discount-screen__block:last-child {
padding-top: 0;
border: 2px solid #066fde;
@@ -111,3 +116,47 @@
width: calc(100% + 32px);
justify-content: center;
}
+
+.discount-screen__widget {
+ background: #fff;
+ bottom: 0;
+ box-shadow: 0 -2px 16px rgba(18, 22, 32, .1);
+ max-width: 428px;
+ width: 100%;
+ padding: 40px;
+ position: relative;
+}
+
+.discount-screen__widget_success {
+ height: 400px;
+}
+
+.discount-screen__success {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ left: 0;
+ top: 0;
+ background: #fff;
+ z-index: 99;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 30px;
+ padding: 40px;
+}
+
+.discount-screen__success-icon {
+ width: 100px;
+ height: 100px;
+ max-width: 50%;
+ flex-shrink: 0;
+}
+
+.discount-screen__success-text {
+ font-size: 24px;
+ line-height: 32px;
+ text-align: center;
+ color: #121620;
+}
diff --git a/src/components/palmistry/discount-screen/discount-screen.tsx b/src/components/palmistry/discount-screen/discount-screen.tsx
index 19e2115..7986465 100644
--- a/src/components/palmistry/discount-screen/discount-screen.tsx
+++ b/src/components/palmistry/discount-screen/discount-screen.tsx
@@ -1,26 +1,99 @@
import React from "react";
import { useNavigate } from 'react-router-dom';
+import { useTranslation } from "react-i18next";
+import { Elements } from "@stripe/react-stripe-js";
+import { Stripe, loadStripe } from "@stripe/stripe-js";
import './discount-screen.css';
import routes from '@/routes';
+import { useApi } from "@/api";
+import { useAuth } from "@/auth";
import HeaderLogo from '@/components/palmistry/header-logo/header-logo';
+import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm";
+
+const currentProductKey = "skip.trial.subscription.aura";
+const returnUrl = `${window.location.host}/palmistry/premium-bundle`;
export default function DiscountScreen() {
const navigate = useNavigate();
+ const api = useApi();
+ const { token, user } = useAuth();
+ const { i18n } = useTranslation();
+ const locale = i18n.language;
- const userHasWeeklySubscription = false;
+ const [price, setPrice] = React.useState('');
+ const [isSuccess] = React.useState(false);
+ const [stripePromise, setStripePromise] = React.useState
| null>(null);
+ const [productId, setProductId] = React.useState('');
+ const [clientSecret, setClientSecret] = React.useState(null);
+ const [stripePublicKey, setStripePublicKey] = React.useState("");
const goPremiumBundle = () => {
navigate(routes.client.palmistryPremiumBundle());
};
React.useEffect(() => {
- if (userHasWeeklySubscription) {
+ (async () => {
+ const { sub_plans } = await api.getSubscriptionPlans({ locale });
+ const plan = sub_plans.find((plan) => plan.id === "stripe.40");
+
+ if (!plan?.price_cents) return;
+
+ setPrice((plan?.price_cents / 100).toFixed(2));
+ })();
+ }, []);
+
+ React.useEffect(() => {
+ (async () => {
+ const products = await api.getSinglePaymentProducts({ token });
+
+ const product = products.find((product) => product.key === currentProductKey);
+
+ if (product) {
+ setProductId(product.productId);
+ }
+ })();
+ }, []);
+
+ React.useEffect(() => {
+ if (!stripePublicKey) return;
+
+ setStripePromise(loadStripe(stripePublicKey));
+ }, [stripePublicKey]);
+
+ const buy = async () => {
+ if (!user?.id) return;
+
+ const response = await api.createSinglePayment({
+ token: token,
+ data: {
+ user: {
+ id: user.id,
+ email: user.email,
+ name: user.username || "",
+ sign: user.profile?.sign?.sign || "",
+ age: user.profile.age?.years || 0,
+ },
+ partner: {
+ sign: "",
+ age: 0,
+ },
+ paymentInfo: {
+ productId,
+ },
+ return_url: returnUrl,
+ },
+ });
+
+ if ('paymentIntent' in response && response.paymentIntent.status === "paid" || 'payment' in response && response.payment.status === "paid") {
goPremiumBundle();
+ } else if ('paymentIntent' in response) {
+ setClientSecret(response.paymentIntent.data.client_secret);
+ setStripePublicKey(response.paymentIntent.data.public_key);
}
- }, [userHasWeeklySubscription]);
+ };
return (
@@ -55,7 +128,7 @@ export default function DiscountScreen() {
save 33%
- €12.73 for
1-week plan
+ €{price} for
1-week plan
Total savings
@@ -67,12 +140,39 @@ export default function DiscountScreen() {
no
-
+
+ {stripePromise && clientSecret && (
+
+
+
+
+
+ {isSuccess && (
+
+
+
+
Payment success
+
+ )}
+
+ )}
);
}
diff --git a/src/components/palmistry/payment-screen/payment-screen.tsx b/src/components/palmistry/payment-screen/payment-screen.tsx
index d4912a4..2895207 100644
--- a/src/components/palmistry/payment-screen/payment-screen.tsx
+++ b/src/components/palmistry/payment-screen/payment-screen.tsx
@@ -1,11 +1,9 @@
import React from "react";
import { useSelector } from "react-redux";
-import { useNavigate } from 'react-router-dom';
import './payment-screen.css';
-import routes from '@/routes';
import useSteps, { Step } from '@/hooks/palmistry/use-steps';
import useTimer from '@/hooks/palmistry/use-timer';
import HeaderLogo from '@/components/palmistry/header-logo/header-logo';
@@ -17,18 +15,16 @@ const getFormattedPrice = (price: number) => {
}
export default function PaymentScreen() {
- const navigate = useNavigate();
const time = useTimer();
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan);
- // const subscriptionStatus = useSelector(selectors.selectStatus);
- const subscriptionStatus = "subscribed";
+ const subscriptionStatus = useSelector(selectors.selectStatus);
const steps = useSteps();
React.useEffect(() => {
if (subscriptionStatus === "subscribed") {
setTimeout(() => {
- navigate(routes.client.palmistryDiscount());
+ steps.goNext();
}, 1500);
}
}, [subscriptionStatus]);
@@ -242,7 +238,7 @@ export default function PaymentScreen() {
{activeSubPlanFromStore && (
- {subscriptionStatus !== "subscribed" &&
}
+ {subscriptionStatus !== "subscribed" &&
}
{subscriptionStatus === "subscribed" && (
diff --git a/src/components/palmistry/premium-bundle-screen/premium-bundle-screen.css b/src/components/palmistry/premium-bundle-screen/premium-bundle-screen.css
index b015746..bbee099 100644
--- a/src/components/palmistry/premium-bundle-screen/premium-bundle-screen.css
+++ b/src/components/palmistry/premium-bundle-screen/premium-bundle-screen.css
@@ -137,3 +137,47 @@
fill: #fff;
margin-right: 8px;
}
+
+.premium-bundle-screen__widget {
+ background: #fff;
+ bottom: 0;
+ box-shadow: 0 -2px 16px rgba(18, 22, 32, .1);
+ max-width: 428px;
+ width: 100%;
+ padding: 40px;
+ position: relative;
+}
+
+.premium-bundle-screen__widget_success {
+ height: 400px;
+}
+
+.premium-bundle-screen__success {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ left: 0;
+ top: 0;
+ background: #fff;
+ z-index: 99;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 30px;
+ padding: 40px;
+}
+
+.premium-bundle-screen__success-icon {
+ width: 100px;
+ height: 100px;
+ max-width: 50%;
+ flex-shrink: 0;
+}
+
+.premium-bundle-screen__success-text {
+ font-size: 24px;
+ line-height: 32px;
+ text-align: center;
+ color: #121620;
+}
diff --git a/src/components/palmistry/premium-bundle-screen/premium-bundle-screen.tsx b/src/components/palmistry/premium-bundle-screen/premium-bundle-screen.tsx
index bed1891..f030979 100644
--- a/src/components/palmistry/premium-bundle-screen/premium-bundle-screen.tsx
+++ b/src/components/palmistry/premium-bundle-screen/premium-bundle-screen.tsx
@@ -1,22 +1,80 @@
import React from "react";
import { useNavigate } from 'react-router-dom';
+import { Elements } from "@stripe/react-stripe-js";
+import { Stripe, loadStripe } from "@stripe/stripe-js";
import './premium-bundle-screen.css';
import routes from '@/routes';
import HeaderLogo from '@/components/palmistry/header-logo/header-logo';
+import { useApi } from '@/api';
+import { useAuth } from "@/auth";
+import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm";
+
+const currentProductKey = "premium.bundle.aura";
+const returnUrl = window.location.host;
export default function PremiumBundleScreen() {
const navigate = useNavigate();
+ const { token, user } = useAuth();
+ const api = useApi();
- const userHasPremiumBundle = false;
+ const [stripePromise, setStripePromise] = React.useState
| null>(null);
+ const [productId, setProductId] = React.useState('');
+ const [isSuccess] = React.useState(false);
+ const [clientSecret, setClientSecret] = React.useState(null);
+ const [stripePublicKey, setStripePublicKey] = React.useState("");
React.useEffect(() => {
- if (userHasPremiumBundle) {
- navigate(routes.client.home());
+ (async () => {
+ const products = await api.getSinglePaymentProducts({ token });
+
+ const product = products.find((product) => product.key === currentProductKey);
+
+ if (product) {
+ setProductId(product.productId);
+ }
+ })();
+ }, []);
+
+ React.useEffect(() => {
+ if (!stripePublicKey) return;
+
+ setStripePromise(loadStripe(stripePublicKey));
+ }, [stripePublicKey]);
+
+ const buy = async () => {
+ if (!user?.id) return;
+
+ const response = await api.createSinglePayment({
+ token: token,
+ data: {
+ user: {
+ id: user.id,
+ email: user.email,
+ name: user.username || "",
+ sign: user.profile?.sign?.sign || "",
+ age: user.profile.age?.years || 0,
+ },
+ partner: {
+ sign: "",
+ age: 0,
+ },
+ paymentInfo: {
+ productId,
+ },
+ return_url: returnUrl,
+ },
+ });
+
+ if ('paymentIntent' in response && response.paymentIntent.status === "paid" || 'payment' in response && response.payment.status === "paid") {
+ goHome();
+ } else if ('paymentIntent' in response) {
+ setClientSecret(response.paymentIntent.data.client_secret);
+ setStripePublicKey(response.paymentIntent.data.public_key);
}
- }, [userHasPremiumBundle]);
+ };
const goHome = () => {
navigate(routes.client.home());
@@ -134,7 +192,10 @@ export default function PremiumBundleScreen() {
-
+
+
+ {stripePromise && clientSecret && (
+
+
+
+
+
+ {isSuccess && (
+
+
+
+
Payment success
+
+ )}
+
+ )}
);
}
diff --git a/src/components/palmistry/step-email/step-email.tsx b/src/components/palmistry/step-email/step-email.tsx
index aa343d4..0a4cee3 100644
--- a/src/components/palmistry/step-email/step-email.tsx
+++ b/src/components/palmistry/step-email/step-email.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
+import { PatchPayload } from "@/api/resources/User";
import { Step } from '@/hooks/palmistry/use-steps';
import { useAuth } from "@/auth";
import { useApi, ApiError, extractErrorMessage } from "@/api";
@@ -55,18 +56,23 @@ export default function StepEmail() {
setIsLoading(true);
const auth = await api.auth({ email, timezone, locale });
const { auth: { token, user } } = auth;
+
signUp(token, user);
- const payload = {
+ const payload: PatchPayload = {
user: {
profile_attributes: {
birthday: steps.getStoredValue(Step.Birthdate),
gender: steps.getStoredValue(Step.Gender),
- relationship_status: steps.getStoredValue(Step.RelationshipStatus),
},
},
token,
};
+ const relationshipStatus = steps.getStoredValue(Step.RelationshipStatus);
+ if (relationshipStatus) {
+ payload.user.profile_attributes!.relationship_status = relationshipStatus;
+ }
+
const updatedUser = await api.updateUser(payload).catch((error) => console.log("Error: ", error));
if (updatedUser?.user) dispatch(actions.user.update(updatedUser.user));
diff --git a/src/components/palmistry/step-paywall/step-paywall.tsx b/src/components/palmistry/step-paywall/step-paywall.tsx
index 1439112..10e7153 100644
--- a/src/components/palmistry/step-paywall/step-paywall.tsx
+++ b/src/components/palmistry/step-paywall/step-paywall.tsx
@@ -1,13 +1,10 @@
import React from 'react';
-import { useNavigate } from "react-router-dom";
import { Step } from '@/hooks/palmistry/use-steps';
import useSteps from '@/hooks/palmistry/use-steps';
import Paywall from '@/components/palmistry/paywall/paywall';
export default function StepPaywall() {
- const navigate = useNavigate();
-
const steps = useSteps();
const storedEmail = steps.getStoredValue(Step.Email);
@@ -18,7 +15,7 @@ export default function StepPaywall() {
}, [storedEmail]);
const onNext = () => {
- navigate('/palmistry/payment');
+ steps.goNext();
};
return (
diff --git a/src/components/palmistry/steps-manager/steps-manager.tsx b/src/components/palmistry/steps-manager/steps-manager.tsx
index dfc43c1..8b1c17f 100644
--- a/src/components/palmistry/steps-manager/steps-manager.tsx
+++ b/src/components/palmistry/steps-manager/steps-manager.tsx
@@ -1,13 +1,9 @@
import React from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { useLocation } from 'react-router-dom';
-import { useSelector } from "react-redux";
-import { useNavigate } from 'react-router-dom';
import './steps-manager.css';
-import routes from '@/routes';
-import { selectors } from "@/store";
import Progressbar from '@/components/palmistry/progress-bar/progress-bar';
import PalmistryContainer from '@/components/palmistry/palmistry-container/palmistry-container';
import useSteps, { Step } from '@/hooks/palmistry/use-steps';
@@ -52,8 +48,6 @@ const animationDuration = 0.2;
export default function StepsManager() {
const steps = useSteps();
const { pathname } = useLocation();
- const subscriptionStatus = useSelector(selectors.selectStatus);
- const navigate = useNavigate();
const [modalIsOpen, setModalIsOpen] = React.useState(false);
@@ -62,12 +56,6 @@ export default function StepsManager() {
steps.goFirstUnpassedStep();
}, [steps.isInited]);
-
- React.useEffect(() => {
- if (subscriptionStatus === "subscribed" && steps.current !== Step.Payment) {
- navigate(routes.client.home());
- }
- }, [subscriptionStatus]);
const motionDivClassName = [
'steps-manager__motion-div',
diff --git a/src/routes.ts b/src/routes.ts
index c760aff..efb882f 100755
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -1,9 +1,14 @@
import type { UserStatus } from "./types";
+const isProduction = import.meta.env.MODE === "production";
+
const host = "";
export const apiHost = "https://api-web.aura.wit.life";
+const dApiHost = isProduction ? "https://d.api.witapps.us" : "https://dev.api.witapps.us"
const siteHost = "https://aura.wit.life";
const prefix = "api/v1";
+const openAIHost = " https://api.openai.com";
+const openAiPrefix = "v1";
const routes = {
client: {
@@ -14,17 +19,22 @@ const routes = {
palmistryBirthdate: () => [host, "palmistry", "birthdate"].join("/"),
palmistryPalmsHold: () => [host, "palmistry", "palms-hold"].join("/"),
palmistryWish: () => [host, "palmistry", "wish"].join("/"),
- palmistryRelationship: () => [host, "palmistry", "relationship-status"].join("/"),
- palmistryResonatedElement: () => [host, "palmistry", "resonated-element"].join("/"),
- palmistryColorYouLike: () => [host, "palmistry", "color-you-like"].join("/"),
+ palmistryRelationship: () =>
+ [host, "palmistry", "relationship-status"].join("/"),
+ palmistryResonatedElement: () =>
+ [host, "palmistry", "resonated-element"].join("/"),
+ palmistryColorYouLike: () =>
+ [host, "palmistry", "color-you-like"].join("/"),
palmistryDecisions: () => [host, "palmistry", "decisions"].join("/"),
palmistryGuidancePlan: () => [host, "palmistry", "guidance-plan"].join("/"),
- palmistryPersonalStatement: () => [host, "palmistry", "personal-statement"].join("/"),
+ palmistryPersonalStatement: () =>
+ [host, "palmistry", "personal-statement"].join("/"),
palmistryScanInfo: () => [host, "palmistry", "scan-info"].join("/"),
palmistryUpload: () => [host, "palmistry", "upload"].join("/"),
palmistryScanPhoto: () => [host, "palmistry", "scan-photo"].join("/"),
palmistryEmail: () => [host, "palmistry", "email"].join("/"),
- palmistrySubscriptionPlan: () => [host, "palmistry", "subscription-plan"].join("/"),
+ palmistrySubscriptionPlan: () =>
+ [host, "palmistry", "subscription-plan"].join("/"),
palmistryPaywall: () => [host, "palmistry", "paywall"].join("/"),
palmistryPayment: () => [host, "palmistry", "payment"].join("/"),
palmistryDiscount: () => [host, "palmistry", "discount"].join("/"),
@@ -108,6 +118,16 @@ const routes = {
unlimitedReadings: () => [host, "unlimited-readings"].join("/"),
addConsultation: () => [host, "add-consultation"].join("/"),
+ // Advisors
+ advisors: () => [host, "advisors"].join("/"),
+ advisorChat: (id: number) => [host, "advisors", id].join("/"),
+ // Email - Pay - Email
+ epeGender: () => [host, "epe", "gender"].join("/"),
+ epeBirthdate: () => [host, "epe", "birthdate"].join("/"),
+ epePayment: () => [host, "epe", "payment"].join("/"),
+ epeSuccessPayment: () => [host, "epe", "success-payment"].join("/"),
+ epeFailPayment: () => [host, "epe", "fail-payment"].join("/"),
+
notFound: () => [host, "404"].join("/"),
},
server: {
@@ -164,6 +184,27 @@ const routes = {
),
getAiRequestsV2: (id: string) =>
[apiHost, "api/v2", "ai", "requests", `${id}.json`].join("/"),
+
+ dApiTestPaymentProducts: () =>
+ [dApiHost, "payment", "test", "products"].join("/"),
+ dApiPaymentCheckout: () => [dApiHost, "payment", "checkout"].join("/"),
+
+ assistants: () => [apiHost, prefix, "ai", "assistants.json"].join("/"),
+ setExternalChatIdAssistants: (chatId: string) =>
+ [apiHost, prefix, "ai", "assistants", chatId, "chats.json"].join("/"),
+ },
+ openAi: {
+ createThread: () => [openAIHost, openAiPrefix, "threads"].join("/"),
+ createMessage: (threadId: string) =>
+ [openAIHost, openAiPrefix, "threads", threadId, "messages"].join("/"),
+ getListMessages: (threadId: string) =>
+ [openAIHost, openAiPrefix, "threads", threadId, "messages"].join("/"),
+ runThread: (threadId: string) =>
+ [openAIHost, openAiPrefix, "threads", threadId, "runs"].join("/"),
+ getStatusRunThread: (threadId: string, runId: string) =>
+ [openAIHost, openAiPrefix, "threads", threadId, "runs", runId].join("/"),
+ getListRuns: (threadId: string) =>
+ [openAIHost, openAiPrefix, "threads", threadId, "runs"].join("/"),
},
};
@@ -182,6 +223,7 @@ export const entrypoints = [
routes.client.magicBall(),
routes.client.trialChoice(),
routes.client.palmistry(),
+ routes.client.advisors(),
];
export const isEntrypoint = (path: string) => entrypoints.includes(path);
export const isNotEntrypoint = (path: string) => !isEntrypoint(path);
@@ -260,15 +302,21 @@ export const withoutFooterRoutes = [
routes.client.trialPaymentWithDiscount(),
routes.client.palmistryPaywall(),
routes.client.palmistryPayment(),
+ routes.client.palmistryDiscount(),
routes.client.email("marketing-landing"),
routes.client.email("marketing-trial-payment"),
routes.client.tryApp(),
routes.client.addReport(),
routes.client.unlimitedReadings(),
routes.client.addConsultation(),
+ routes.client.advisors(),
+ routes.client.epeSuccessPayment(),
];
-export const withoutFooterPartOfRoutes = [routes.client.questionnaire()];
+export const withoutFooterPartOfRoutes = [
+ routes.client.questionnaire(),
+ routes.client.advisors(),
+];
export const hasNoFooter = (path: string) => {
const targetRoute = withoutFooterPartOfRoutes.findIndex((route) =>
@@ -285,6 +333,7 @@ export const withNavbarFooterRoutes = [
routes.client.breath(),
routes.client.breathResult(),
routes.client.wallpaper(),
+ routes.client.advisors(),
];
export const hasNavbarFooter = (path: string) =>
withNavbarFooterRoutes.includes(path);
@@ -334,14 +383,16 @@ export const withoutHeaderRoutes = [
routes.client.email("marketing-landing"),
routes.client.email("marketing-trial-payment"),
routes.client.tryApp(),
+ routes.client.advisors(),
+ routes.client.epeSuccessPayment(),
];
export const hasNoHeader = (path: string) => {
let result = true;
withoutHeaderRoutes.forEach((route) => {
if (
- !path.includes("palmistry") && path.includes(route) ||
- path.includes("palmistry") && path === route
+ (!path.includes("palmistry") && path.includes(route)) ||
+ (path.includes("palmistry") && path === route)
) {
result = false;
}
diff --git a/src/services/price/index.ts b/src/services/price/index.ts
index 053aec7..01cb9f0 100755
--- a/src/services/price/index.ts
+++ b/src/services/price/index.ts
@@ -22,3 +22,7 @@ export const getPriceFromTrial = (trial: ITrial | null) => {
}
return (trial.price_cents === 100 ? 99 : trial.price_cents || 0) / 100;
};
+
+export const getPriceCentsToDollars = (cents: number) => {
+ return (cents / 100).toFixed(2);
+};
diff --git a/src/store/index.ts b/src/store/index.ts
index 7eeca3f..d19a036 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -16,6 +16,7 @@ import form, {
import aura, { actions as auraActions } from "./aura";
import siteConfig, {
selectHome,
+ selectOpenAiToken,
actions as siteConfigActions,
} from "./siteConfig";
import onboardingConfig, {
@@ -105,6 +106,7 @@ export const selectors = {
selectQuestionnaire,
selectUserDeviceType,
selectIsShowTryApp,
+ selectOpenAiToken,
...formSelectors,
};
diff --git a/src/store/siteConfig.ts b/src/store/siteConfig.ts
index 485aacf..8438500 100644
--- a/src/store/siteConfig.ts
+++ b/src/store/siteConfig.ts
@@ -12,6 +12,7 @@ interface ISiteConfig {
isShowNavbar: boolean;
pathFromHome: EPathsFromHome;
};
+ openAiToken: string;
}
const initialState: ISiteConfig = {
@@ -19,6 +20,7 @@ const initialState: ISiteConfig = {
isShowNavbar: false,
pathFromHome: EPathsFromHome.compatibility,
},
+ openAiToken: "",
};
const siteConfigSlice = createSlice({
@@ -37,4 +39,8 @@ export const selectHome = createSelector(
(state: { siteConfig: ISiteConfig }) => state.siteConfig.home,
(siteConfig) => siteConfig
);
+export const selectOpenAiToken = createSelector(
+ (state: { siteConfig: ISiteConfig }) => state.siteConfig.openAiToken,
+ (siteConfig) => siteConfig
+);
export default siteConfigSlice.reducer;