diff --git a/index.html b/index.html index f1dab0c..2d0d1da 100755 --- a/index.html +++ b/index.html @@ -58,11 +58,11 @@ region: "eu", }); --> - + > --> diff --git a/package-lock.json b/package-lock.json index e1139c6..7dc1d88 100755 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@chargebee/chargebee-js-react-wrapper": "^0.6.3", "@reduxjs/toolkit": "^1.9.5", + "@smakss/react-scroll-direction": "^4.0.4", "@stripe/react-stripe-js": "^2.3.1", "@stripe/stripe-js": "^2.1.9", "apng-js": "^1.1.1", @@ -1022,6 +1023,17 @@ "node": ">=14" } }, + "node_modules/@smakss/react-scroll-direction": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smakss/react-scroll-direction/-/react-scroll-direction-4.0.4.tgz", + "integrity": "sha512-FtzjZTJTLFN9A0mcJk7dXgYHFlGVPXW/EJooSVbe2dHU5hAi5rFk0ODimB7pHeHoDIUin5zE1NDtU2eY6olwlA==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@stripe/react-stripe-js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.3.1.tgz", @@ -4195,6 +4207,12 @@ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.6.2.tgz", "integrity": "sha512-LzqpSrMK/3JBAVBI9u3NWtOhWNw5AMQfrUFYB0+bDHTSw17z++WJLsPsxAuK+oSddsxk4d7F/JcdDPM1M5YAhA==" }, + "@smakss/react-scroll-direction": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smakss/react-scroll-direction/-/react-scroll-direction-4.0.4.tgz", + "integrity": "sha512-FtzjZTJTLFN9A0mcJk7dXgYHFlGVPXW/EJooSVbe2dHU5hAi5rFk0ODimB7pHeHoDIUin5zE1NDtU2eY6olwlA==", + "requires": {} + }, "@stripe/react-stripe-js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.3.1.tgz", diff --git a/package.json b/package.json index c66ba58..03a0ec7 100755 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@chargebee/chargebee-js-react-wrapper": "^0.6.3", "@reduxjs/toolkit": "^1.9.5", + "@smakss/react-scroll-direction": "^4.0.4", "@stripe/react-stripe-js": "^2.3.1", "@stripe/stripe-js": "^2.1.9", "apng-js": "^1.1.1", diff --git a/public/star.png b/public/star.png new file mode 100644 index 0000000..003bd4f Binary files /dev/null and b/public/star.png differ diff --git a/public/star.svg b/public/star.svg new file mode 100644 index 0000000..eab0dd5 --- /dev/null +++ b/public/star.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/api/api.ts b/src/api/api.ts index abba855..929a879 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -23,7 +23,10 @@ import { GoogleAuth, SubscriptionPlans, AppleAuth, - AIRequestsV2 + AIRequestsV2, + Assistants, + OpenAI, + SinglePayment } from './resources' const api = { @@ -55,6 +58,17 @@ const api = { getZodiacs: createMethod(Zodiacs.createRequest), AIRequestsV2: createMethod(AIRequestsV2.createRequest), getAIRequestsV2: createMethod(AIRequestsV2.createRequestGet), + // Advisors chats + assistants: createMethod(Assistants.createRequest), + setExternalChatIdAssistant: createMethod(Assistants.createRequestSetExternalChatId), + createThread: createMethod(OpenAI.createRequest), + createMessage: createMethod(OpenAI.createRequest), + getListMessages: createMethod(OpenAI.createRequest), + runThread: createMethod(OpenAI.createRequest), + getStatusRunThread: createMethod(OpenAI.createRequest), + getListRuns: createMethod(OpenAI.createRequest), + getSinglePaymentProducts: createMethod(SinglePayment.createRequestGet), + createSinglePayment: createMethod(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)} + /> Secure - - {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<NodeJS.Timeout>(); 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 <div className={styles.page}>{isLoading && <Loader />}</div>; 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 ( + <div className={`${styles.container} ${classNameContainer}`}> + <div className={styles["back-button"]} onClick={clickBackButton}> + <div className={styles["arrow"]} /> Advisors + </div> + <div className={styles.name}> + {name} + <span className={styles["online-status"]} /> + </div> + <img className={styles.avatar} src={avatar} alt={name} /> + </div> + ); +} + +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<HTMLTextAreaElement>) => void; + submitForm: (messageText: string) => void; +} + +function InputMessage({ + placeholder, + messageText, + textareaRows, + disabledButton, + disabledTextArea, + isLoading, + handleChangeMessageText, + submitForm, + classNameContainer = "", +}: IInputMessageProps) { + const handleSubmit = (e: FormEvent<HTMLFormElement>) => { + e.preventDefault(); + submitForm(messageText); + }; + + return ( + <form + className={`${styles.container} ${classNameContainer}`} + onSubmit={(e) => handleSubmit(e)} + > + <textarea + className={styles.input} + disabled={disabledTextArea} + value={messageText} + onChange={(e) => handleChangeMessageText(e)} + rows={textareaRows} + placeholder={placeholder || "Type your message"} + /> + {!isLoading && ( + <button className={styles.button} disabled={disabledButton}> + <div className={styles["arrow"]} /> + </button> + )} + {isLoading && <Loader color={LoaderColor.White} />} + </form> + ); +} + +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 ( + <div className={styles.container} /> + ) +} + +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 ( + <div + className={styles.container} + style={{ flexDirection: isSelf ? "row-reverse" : "row" }} + > + {!isSelf && avatar?.length && ( + <img + className={styles.avatar} + src={avatar} + alt={`${advisorName} avatar`} + /> + )} + <div + className={styles.text} + style={{ color: textColor, backgroundColor: backgroundTextColor }} + > + {text} + </div> + </div> + ); +} + +// 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<IAssistant>(); + const [messageText, setMessageText] = useState(""); + const [textareaRows, setTextareaRows] = useState(1); + const [messages, setMessages] = useState<IMessage[]>([]); + const [isLoading, setIsLoading] = useState(true); + const [isLoadingSelfMessage, setIsLoadingSelfMessage] = useState(false); + const [isLoadingAdvisorMessage, setIsLoadingAdvisorMessage] = useState(false); + const [isLoadingLatestMessages, setIsLoadingLatestMessages] = useState(false); + const timeOutStatusRunRef = useRef<NodeJS.Timeout>(); + const messagesEndRef = useRef<HTMLDivElement>(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<Assistants.Response>(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<ResponseRunThread> => { + 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<HTMLTextAreaElement>) => { + 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 ( + <section className={`${styles.page} page`}> + {isLoading && ( + <Loader color={LoaderColor.Red} className={styles.loader} /> + )} + {!isLoading && ( + <ChatHeader + name={assistant?.name || ""} + avatar={assistant?.photo?.th2x || ""} + classNameContainer={styles["header-container"]} + clickBackButton={() => navigate(-1)} + /> + )} + {!!messages.length && ( + <div className={styles["messages-container"]}> + {isLoadingAdvisorMessage && ( + <Message + avatar={assistant?.photo?.th2x || ""} + text={<LoaderDots />} + isSelf={false} + backgroundTextColor={"#c9c9c9"} + textColor={"#000"} + /> + )} + {messages.map((message) => + message.content.map((content) => ( + <Message + avatar={assistant?.photo?.th2x || ""} + text={content.text.value} + advisorName={assistant?.name || ""} + backgroundTextColor={ + getIsSelfMessage(message.role) ? "#0080ff" : "#c9c9c9" + } + textColor={getIsSelfMessage(message.role) ? "#fff" : "#000"} + isSelf={getIsSelfMessage(message.role)} + key={message.id} + /> + )) + )} + <div className={styles["loader-container"]}> + {isLoadingLatestMessages && <Loader color={LoaderColor.Red} />} + </div> + </div> + )} + + <div ref={messagesEndRef} /> + {!isLoading && ( + <InputMessage + placeholder="Text message" + messageText={messageText} + textareaRows={textareaRows} + disabledTextArea={ + isLoadingAdvisorMessage || isLoadingSelfMessage || isLoading + } + disabledButton={!messageText.length} + classNameContainer={styles["input-container"]} + handleChangeMessageText={handleChangeMessageText} + isLoading={isLoadingSelfMessage} + submitForm={sendMessage} + /> + )} + </section> + ); +} + +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 ( + <div className={styles.container} onClick={onClickCard}> + <div + className={styles.header} + style={{ backgroundImage: `url(${th2x})` }} + > + <Title variant="h3" className={styles.name}> + {name} + <span className={styles["online-status"]} /> + +
+
+ {expirience} +
+
+ {Array(stars) + .fill(0) + .map((_, index) => ( + star + ))} + | {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 ( +
+ Exclamation Icon +
+ {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 ( +
+ Success Icon +
+ 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 && ( + Success Icon + )} + + + )} + {(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})`, }} > - svg-animation + + svg-animation +
{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 && ( <div> - <p className={styles["sub-plan-description"]}> - You will be charged only{" "} - <b> - ${getPriceFromTrial(activeSubPlan?.trial)} for your 3-day trial. - </b> - </p> - <p className={styles["sub-plan-description"]}> - We`ll <b>email you a reminder</b> before your trial period ends. - </p> + {!noTrial && ( + <> + <p className={styles["sub-plan-description"]}> + You will be charged only{" "} + <b> + ${getPriceFromTrial(activeSubPlan?.trial)} for your 3-day trial. + </b> + </p> + <p className={styles["sub-plan-description"]}> + We`ll <b>email you a reminder</b> before your trial period ends. + </p> + </> + )} + <p className={styles["sub-plan-description"]}> Cancel anytime. The charge will appear on your bill as witapps. </p> </div> )} <div className={styles["payment-method-container"]}> - {stripePromise && clientSecret && subscriptionReceiptId && ( + {stripePromise && clientSecret && ( <Elements stripe={stripePromise} options={{ clientSecret }}> {selectedPaymentMethod === EPaymentMethod.PAYPAL_OR_APPLE_PAY && ( <div className={styles["payment-method"]}> @@ -199,13 +209,14 @@ function PaymentModal({ activeSubscriptionPlan }: IPaymentModalProps) { activeSubPlan={activeSubPlan} client_secret={clientSecret} subscriptionReceiptId={subscriptionReceiptId} + returnUrl={window.location.href} /> {!!errors.length && <p className={styles.errors}>{errors}</p>} </div> )} {selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && ( - <CheckoutForm subscriptionReceiptId={subscriptionReceiptId} /> + <CheckoutForm subscriptionReceiptId={subscriptionReceiptId} returnUrl={returnUrl} /> )} </Elements> )} 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<Promise<Stripe | null> | null>(null); + const [productId, setProductId] = React.useState(''); + const [clientSecret, setClientSecret] = React.useState<string | null>(null); + const [stripePublicKey, setStripePublicKey] = React.useState<string>(""); 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 ( <div className="discount-screen"> @@ -55,7 +128,7 @@ export default function DiscountScreen() { <section className="discount-screen__block"> <div className="discount-screen__header-block">save 33%</div> - <span className="discount-screen__price-block">€12.73 for <br /> 1-week plan</span> + <span className="discount-screen__price-block">€{price} for <br /> 1-week plan</span> <div className="discount-screen__details"> <span className="discount-screen__details-name">Total savings</span> @@ -67,12 +140,39 @@ export default function DiscountScreen() { <span className="discount-screen__details-value">no</span> </div> - <button className="discount-screen__button"> + <button className="discount-screen__button" onClick={buy}> Pay now and <br /> skip trial </button> </section> </div> </div> + + {stripePromise && clientSecret && ( + <div className={`discount-screen__widget${isSuccess ? " discount-screen__widget_success" : ""}`}> + <Elements stripe={stripePromise} options={{ clientSecret }}> + <CheckoutForm returnUrl={returnUrl} /> + </Elements> + + {isSuccess && ( + <div className="discount-screen__success"> + <svg + className="discount-screen__success-icon" + xmlns="http://www.w3.org/2000/svg" + width="512" + height="512" + viewBox="0 0 52 52" + > + <path + fill="#4ec794" + d="M26 0C11.664 0 0 11.663 0 26s11.664 26 26 26 26-11.663 26-26S40.336 0 26 0zm14.495 17.329-16 18a1.997 1.997 0 0 1-2.745.233l-10-8a2 2 0 0 1 2.499-3.124l8.517 6.813L37.505 14.67a2.001 2.001 0 0 1 2.99 2.659z" + /> + </svg> + + <div className="discount-screen__success-text">Payment success</div> + </div> + )} + </div> + )} </div> ); } 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 && ( <div className={`payment-screen__widget${subscriptionStatus === "subscribed" ? " payment-screen__widget_success" : ""}`}> - {subscriptionStatus !== "subscribed" && <PaymentModal />} + {subscriptionStatus !== "subscribed" && <PaymentModal returnUrl={window.location.href}/>} {subscriptionStatus === "subscribed" && ( <div className="payment-screen__success"> 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<Promise<Stripe | null> | null>(null); + const [productId, setProductId] = React.useState(''); + const [isSuccess] = React.useState(false); + const [clientSecret, setClientSecret] = React.useState<string | null>(null); + const [stripePublicKey, setStripePublicKey] = React.useState<string>(""); 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() { </div> </div> - <button className="premium-bundle-screen__button premium-bundle-screen__button-active premium-bundle-screen__buy-button"> + <button + className="premium-bundle-screen__button premium-bundle-screen__button-active premium-bundle-screen__buy-button" + onClick={buy} + > <svg width="13" height="16" @@ -151,6 +212,33 @@ export default function PremiumBundleScreen() { Buy now </button> </div> + + {stripePromise && clientSecret && ( + <div className={`discount-screen__widget${isSuccess ? " discount-screen__widget_success" : ""}`}> + <Elements stripe={stripePromise} options={{ clientSecret }}> + <CheckoutForm returnUrl={returnUrl} /> + </Elements> + + {isSuccess && ( + <div className="discount-screen__success"> + <svg + className="discount-screen__success-icon" + xmlns="http://www.w3.org/2000/svg" + width="512" + height="512" + viewBox="0 0 52 52" + > + <path + fill="#4ec794" + d="M26 0C11.664 0 0 11.663 0 26s11.664 26 26 26 26-11.663 26-26S40.336 0 26 0zm14.495 17.329-16 18a1.997 1.997 0 0 1-2.745.233l-10-8a2 2 0 0 1 2.499-3.124l8.517 6.813L37.505 14.67a2.001 2.001 0 0 1 2.99 2.659z" + /> + </svg> + + <div className="discount-screen__success-text">Payment success</div> + </div> + )} + </div> + )} </div> ); } 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;