From 06d85aedfd6b95ec7aa7e1ab970cb7214524093c Mon Sep 17 00:00:00 2001 From: Daniil Chemerkin Date: Wed, 17 Apr 2024 23:29:56 +0000 Subject: [PATCH 01/10] Develop --- src/api/useApiCall.ts | 7 +- src/components/App/index.tsx | 147 +++++++++++++++--- src/components/BirthdayPage/index.tsx | 4 +- src/components/EmailEnterPage/index.tsx | 28 ++-- src/components/pages/BirthPlacePage/index.tsx | 4 +- .../pages/SinglePaymentPage/index.tsx | 81 ++++++++++ .../pages/SinglePaymentPage/styles.module.css | 0 src/routes.ts | 6 +- 8 files changed, 241 insertions(+), 36 deletions(-) create mode 100644 src/components/pages/SinglePaymentPage/index.tsx create mode 100644 src/components/pages/SinglePaymentPage/styles.module.css diff --git a/src/api/useApiCall.ts b/src/api/useApiCall.ts index f82f06a..8da2382 100644 --- a/src/api/useApiCall.ts +++ b/src/api/useApiCall.ts @@ -11,10 +11,13 @@ interface HookResult { type ApiMethod = () => Promise type ApiCallState = 'idle' | 'pending' | 'success' | 'error' -export function useApiCall(apiMethod: ApiMethod): HookResult { +export function useApiCall( + apiMethod: ApiMethod, + startState: ApiCallState = 'idle' +): HookResult { const [data, setData] = useState(null) const [error, setError] = useState(null) - const [state, setState] = useState('idle') + const [state, setState] = useState(startState) const isPending = state === 'pending' useEffect(() => { diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 74055fd..f834438 100755 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -113,7 +113,6 @@ import AddConsultationPage from "../pages/AdditionalPurchases/pages/AddConsultat 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"; import { useSchemeColorByElement } from "@/hooks/useSchemeColorByElement"; @@ -121,6 +120,7 @@ import GetInformationPartnerPage from "../pages/GetInformationPartner"; import BirthPlacePage from "../pages/BirthPlacePage"; import LoadingPage from "../pages/LoadingPage"; import { EProductKeys, productUrls } from "@/data/products"; +import SinglePaymentPage from "../pages/SinglePaymentPage"; const isProduction = import.meta.env.MODE === "production"; @@ -146,7 +146,7 @@ function App(): JSX.Element { } else { dispatch(actions.userConfig.addIsForceShortPath(false)); } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const isForceShortPath = useSelector(selectors.selectIsForceShortPath); @@ -276,10 +276,10 @@ function App(): JSX.Element { force: routes.client.epeBirthdate(), }, purchasedProduct: { - no: routes.client.singlePaymentShortPath("moons.pdf.aura"), + no: routes.client.epePayment(), }, }} - requiredParameters={[]} + requiredParameters={[isForceShortPath || gender]} /> } > @@ -297,7 +297,7 @@ function App(): JSX.Element { no: routes.client.epeGender(), }, purchasedProduct: { - no: routes.client.singlePaymentShortPath("moons.pdf.aura"), + no: routes.client.epePayment(), }, }} requiredParameters={[isForceShortPath || gender]} @@ -309,6 +309,54 @@ function App(): JSX.Element { element={} /> + + } + > + + } + /> + + + } + > + + } + /> + } > @@ -373,7 +421,7 @@ function App(): JSX.Element { no: routes.client.advisorChatGender(), }, purchasedProduct: { - no: routes.client.singlePaymentShortPath("chat.aura"), + no: routes.client.advisorChatPayment(), }, }} requiredParameters={[isForceShortPath || gender]} @@ -395,7 +443,7 @@ function App(): JSX.Element { force: routes.client.advisorChatBirthdate(), }, purchasedProduct: { - no: routes.client.singlePaymentShortPath("chat.aura"), + no: routes.client.advisorChatPayment(), }, }} requiredParameters={[birthdate, isForceShortPath || gender]} @@ -417,7 +465,7 @@ function App(): JSX.Element { force: routes.client.advisorChatBirthdate(), }, purchasedProduct: { - no: routes.client.singlePaymentShortPath("chat.aura"), + no: routes.client.advisorChatPayment(), }, }} requiredParameters={[birthdate, isForceShortPath || gender]} @@ -429,6 +477,68 @@ function App(): JSX.Element { element={} /> + + + } + > + + } + /> + + + + } + > + + } + /> + + } @@ -443,15 +553,14 @@ function App(): JSX.Element { productKey={EProductKeys["chat.aura"]} redirectUrls={{ user: { - no: routes.client.advisorChatGender(), - force: routes.client.advisorChatBirthdate(), + no: routes.client.advisorChatEmail(), }, data: { no: routes.client.advisorChatGender(), force: routes.client.advisorChatBirthdate(), }, purchasedProduct: { - no: routes.client.singlePaymentShortPath("chat.aura"), + no: routes.client.advisorChatPayment(), }, }} requiredParameters={[ @@ -470,7 +579,7 @@ function App(): JSX.Element { {/* Advisor short path */} {/* Single Payment Page Short Path */} - } > } /> - + */} {/* Single Payment Page Short Path */} {/* Test Routes Start */} @@ -990,9 +1099,9 @@ function ShortPathOutlet(props: IShortPathOutletProps): JSX.Element { }; } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [token]); - const { data, isPending } = useApiCall(loadData); + const { data, isPending } = useApiCall(loadData, "pending"); if (isPending) { return ; diff --git a/src/components/BirthdayPage/index.tsx b/src/components/BirthdayPage/index.tsx index 27f4c3e..b6ce9fc 100644 --- a/src/components/BirthdayPage/index.tsx +++ b/src/components/BirthdayPage/index.tsx @@ -19,7 +19,7 @@ function BirthdayPage(): JSX.Element { const [isDisabled, setIsDisabled] = useState(true); let nextRoute = routes.client.didYouKnow(); if (window.location.href.includes("/epe/")) { - nextRoute = routes.client.singlePaymentShortPath("moons.pdf.aura"); + nextRoute = routes.client.epeEmail(); } if (window.location.href.includes("/advisor-chat/")) { nextRoute = routes.client.advisorChatBirthtime(); @@ -84,4 +84,4 @@ function BirthdayPage(): JSX.Element { ); } -export default BirthdayPage; +export default BirthdayPage; \ No newline at end of file diff --git a/src/components/EmailEnterPage/index.tsx b/src/components/EmailEnterPage/index.tsx index c262d42..13d7228 100755 --- a/src/components/EmailEnterPage/index.tsx +++ b/src/components/EmailEnterPage/index.tsx @@ -17,7 +17,15 @@ import routes from "@/routes"; import NameInput from "./NameInput"; import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans"; -function EmailEnterPage(): JSX.Element { +interface IEmailEnterPage { + redirectUrl?: string; + isRequiredName?: boolean; +} + +function EmailEnterPage({ + redirectUrl = routes.client.emailConfirm(), + isRequiredName = false, +}: IEmailEnterPage): JSX.Element { const api = useApi(); const { signUp } = useAuth(); const { t, i18n } = useTranslation(); @@ -28,7 +36,7 @@ function EmailEnterPage(): JSX.Element { const birthday = useSelector(selectors.selectBirthday); const [isDisabled, setIsDisabled] = useState(true); const [isValidEmail, setIsValidEmail] = useState(false); - const [isValidName, setIsValidName] = useState(true); + const [isValidName, setIsValidName] = useState(!isRequiredName); const [isLoading, setIsLoading] = useState(false); const [isAuth, setIsAuth] = useState(false); const [apiError, setApiError] = useState(null); @@ -41,9 +49,11 @@ function EmailEnterPage(): JSX.Element { const timezone = getClientTimezone(); const locale = i18n.language; const { subPlan } = useParams(); - const { gender, flowChoice, birthPlace } = useSelector( - selectors.selectQuestionnaire - ); + const { + gender, + birthPlace, + // flowChoice + } = useSelector(selectors.selectQuestionnaire); useEffect(() => { if (subPlan) { @@ -117,9 +127,9 @@ function EmailEnterPage(): JSX.Element { user: { profile_attributes: { birthday, - gender, + gender: gender.length ? gender : "male", full_name: name, - relationship_status: flowChoice, + // relationship_status: !!flowChoice.length ? flowChoice : null, }, birthplace_attributes: { address: birthPlace }, }, @@ -147,7 +157,7 @@ function EmailEnterPage(): JSX.Element { setIsLoading(false); setIsAuth(true); setTimeout(() => { - navigate(routes.client.emailConfirm()); + navigate(redirectUrl); }, 1000); } catch (error) { console.error(error); @@ -170,7 +180,7 @@ function EmailEnterPage(): JSX.Element { value={name} placeholder="Your name" onValid={handleValidName} - onInvalid={() => setIsValidName(true)} + onInvalid={() => setIsValidName(!isRequiredName)} /> { - navigate(routes.client.singlePaymentShortPath("chat.aura")); + navigate(routes.client.advisorChatEmail()); }; return ( @@ -45,4 +45,4 @@ function BirthPlacePage() { ); } -export default BirthPlacePage; +export default BirthPlacePage; \ No newline at end of file diff --git a/src/components/pages/SinglePaymentPage/index.tsx b/src/components/pages/SinglePaymentPage/index.tsx new file mode 100644 index 0000000..195b814 --- /dev/null +++ b/src/components/pages/SinglePaymentPage/index.tsx @@ -0,0 +1,81 @@ +import Title from "@/components/Title"; +import styles from "./styles.module.css"; +import PaymentForm from "../PaymentWithEmailPage/PaymentForm"; +import { getPriceCentsToDollars } from "@/services/price"; +import { useSinglePayment } from "@/hooks/payment/useSinglePayment"; +import routes from "@/routes"; +import { useAuth } from "@/auth"; +import Loader, { LoaderColor } from "@/components/Loader"; +import { useCallback, useEffect } from "react"; +import { EProductKeys } from "@/data/products"; + +interface ISinglePaymentPage { + productId: EProductKeys; + isForce?: boolean; +} + +function SinglePaymentPage({ productId, isForce = false }: ISinglePaymentPage) { + const { + product, + paymentIntent, + isLoading: isLoadingSinglePayment, + error: errorSinglePayment, + createSinglePayment, + } = useSinglePayment(); + const { user: userFromStore, token: tokenFromStore } = useAuth(); + + const returnUrl = `${window.location.protocol}//${ + window.location.host + }${routes.client.paymentResult()}`; + + const createPayment = useCallback(async () => { + if (!tokenFromStore.length || !userFromStore) { + return; + } + await createSinglePayment({ + user: userFromStore, + token: tokenFromStore, + targetProductKey: productId || "", + returnUrl, + }); + }, [ + createSinglePayment, + productId, + returnUrl, + tokenFromStore, + userFromStore, + ]); + + useEffect(() => { + createPayment(); + }, [createPayment]); + + return ( +
+ {isLoadingSinglePayment && } + {!isLoadingSinglePayment && + paymentIntent && + "paymentIntent" in paymentIntent && + !!tokenFromStore.length && ( + <> + + {getPriceCentsToDollars(product?.amount || 0)}$ + + + + )} + {errorSinglePayment?.error && ( + + Something went wrong:{" "} + {errorSinglePayment?.error?.length && errorSinglePayment?.error} + + )} +
+ ); +} + +export default SinglePaymentPage; \ No newline at end of file diff --git a/src/components/pages/SinglePaymentPage/styles.module.css b/src/components/pages/SinglePaymentPage/styles.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/routes.ts b/src/routes.ts index de38b8e..94f3ad3 100755 --- a/src/routes.ts +++ b/src/routes.ts @@ -6,7 +6,7 @@ const host = ""; export const apiHost = "https://api-web.aura.wit.life"; const dApiHost = isProduction ? "https://api.aura.witapps.us" - : "https://dev.api.witapps.us"; + : "https://dev.api.aura.witapps.us"; // const dApiHost = "https://d.api.witapps.us"; const siteHost = "https://aura.wit.life"; const prefix = "api/v1"; @@ -128,6 +128,7 @@ const routes = { // Email - Pay - Email epeGender: () => [host, "epe", "gender"].join("/"), epeBirthdate: () => [host, "epe", "birthdate"].join("/"), + epeEmail: () => [host, "epe", "email"].join("/"), epePayment: () => [host, "epe", "payment"].join("/"), epeSuccessPayment: () => [host, "epe", "success-payment"].join("/"), epeFailPayment: () => [host, "epe", "fail-payment"].join("/"), @@ -138,6 +139,7 @@ const routes = { advisorChatBirthtime: () => [host, "advisor-chat", "birthtime"].join("/"), advisorChatBirthPlace: () => [host, "advisor-chat", "birth-place"].join("/"), + advisorChatEmail: () => [host, "advisor-chat", "email"].join("/"), advisorChatPayment: () => [host, "advisor-chat", "payment"].join("/"), advisorChatSuccessPayment: () => [host, "advisor-chat", "success-payment"].join("/"), @@ -459,4 +461,4 @@ export const getRouteBy = (status: UserStatus): string => { } }; -export default routes; +export default routes; \ No newline at end of file From d45f3a301fb8794db7963069454eb5aa3751e965 Mon Sep 17 00:00:00 2001 From: Daniil Chemerkin Date: Wed, 17 Apr 2024 23:31:02 +0000 Subject: [PATCH 02/10] Develop From fab0130ac8a714551656eb49ca5c7d4ef4c33bd3 Mon Sep 17 00:00:00 2001 From: Daniil Chemerkin Date: Wed, 17 Apr 2024 23:31:33 +0000 Subject: [PATCH 03/10] Develop From cace7a41f5f0cd74c43635b316e90ddd9aa69956 Mon Sep 17 00:00:00 2001 From: Daniil Chemerkin Date: Wed, 17 Apr 2024 23:32:05 +0000 Subject: [PATCH 04/10] Develop From 8bf0d5312fa257d05eb9f19397eabf21b683357e Mon Sep 17 00:00:00 2001 From: Daniil Chemerkin Date: Thu, 18 Apr 2024 21:30:01 +0000 Subject: [PATCH 05/10] Develop --- src/components/palmistry/modal/modal.tsx | 23 ++++++--- .../palm-camera-modal/palm-camera-modal.css | 21 +++++++- .../palm-camera-modal/palm-camera-modal.tsx | 49 ++++++++++++------- vite.config.ts | 13 ++++- 4 files changed, 79 insertions(+), 27 deletions(-) diff --git a/src/components/palmistry/modal/modal.tsx b/src/components/palmistry/modal/modal.tsx index 86ccb0e..50f804e 100644 --- a/src/components/palmistry/modal/modal.tsx +++ b/src/components/palmistry/modal/modal.tsx @@ -1,28 +1,39 @@ -import './modal.css'; +import "./modal.css"; export enum ModalType { - Error = 'error', + Error = "error", } type Props = { children: React.ReactNode; type?: ModalType; noCloseButton?: boolean; + modalClassName?: string; onClose: () => void; }; export default function Modal(props: Props) { - const className = ['modal']; + const className = ["modal"]; + + if (props.modalClassName?.length) { + className.push(props.modalClassName); + } if (props.type === ModalType.Error) { - className.push('modal_type_error'); + className.push("modal_type_error"); } return ( -
+
{!props.noCloseButton && (
- + void; @@ -13,7 +13,9 @@ type Props = { export default function PalmCameraModal(props: Props) { const videoEl = React.useRef(null); - const [mediaStream, setMediaStream] = React.useState(null); + const [mediaStream, setMediaStream] = React.useState( + null + ); const onClickOverlay = (e: React.MouseEvent) => { if (e.target === e.currentTarget) { @@ -26,20 +28,20 @@ export default function PalmCameraModal(props: Props) { try { const stream = await navigator.mediaDevices.getUserMedia({ - video: { facingMode: { ideal: 'environment' } }, - }) + video: { facingMode: { ideal: "environment" } }, + }); setMediaStream(stream); videoEl.current.srcObject = stream; - videoEl.current.addEventListener('loadedmetadata', videoEl.current.play); - } catch(error) { - console.error('Camera is not available', error); + videoEl.current.addEventListener("loadedmetadata", videoEl.current.play); + } catch (error) { + console.error("Camera is not available", error); } }; const deactivateCamera = React.useCallback(() => { - mediaStream?.getTracks().forEach(track => track.stop()); + mediaStream?.getTracks().forEach((track) => track.stop()); }, [mediaStream]); React.useEffect(() => { @@ -49,9 +51,9 @@ export default function PalmCameraModal(props: Props) { }, []); const takePhoto = () => { - const canvas = document.createElement('canvas'); + const canvas = document.createElement("canvas"); - const context = canvas.getContext('2d'); + const context = canvas.getContext("2d"); if (!context || !videoEl.current) return null; @@ -62,7 +64,7 @@ export default function PalmCameraModal(props: Props) { canvas.height = height; context.drawImage(videoEl.current, 0, 0, width, height); - const data = canvas.toDataURL('image/png'); + const data = canvas.toDataURL("image/png"); return data; }; @@ -78,9 +80,15 @@ export default function PalmCameraModal(props: Props) { }; return ( - - -
+ + +
-