From 0a315b1b135441962ec7d851f8bc701eb6251cc5 Mon Sep 17 00:00:00 2001 From: Daniil Chemerkin Date: Mon, 14 Oct 2024 15:12:22 +0000 Subject: [PATCH] Develop --- src/api/api.ts | 6 +- src/api/resources/Login.ts | 27 ++ src/api/resources/Password.ts | 22 ++ src/api/resources/index.ts | 2 + src/components/App/index.tsx | 27 +- src/components/HomePage/index.tsx | 3 + .../pages/GenderPalmistry/index.tsx | 2 + .../v1/pages/EmailEnterPage/PasswordInput.tsx | 51 ++++ .../pages/ABDesign/v1/pages/Gender/index.tsx | 2 + .../pages/Auth/ResetYourPassword/index.tsx | 40 +++ .../Auth/ResetYourPassword/styles.module.scss | 12 + src/components/pages/Auth/index.tsx | 250 ++++++++++++++++++ src/components/pages/Auth/styles.module.scss | 128 +++++++++ .../ui/AlreadyHaveAccount/index.tsx | 18 ++ .../ui/AlreadyHaveAccount/styles.module.scss | 9 + .../authentication/use-authentication.ts | 56 +++- src/routes.ts | 6 +- 17 files changed, 645 insertions(+), 16 deletions(-) create mode 100644 src/api/resources/Login.ts create mode 100644 src/api/resources/Password.ts create mode 100644 src/components/pages/ABDesign/v1/pages/EmailEnterPage/PasswordInput.tsx create mode 100644 src/components/pages/Auth/ResetYourPassword/index.tsx create mode 100644 src/components/pages/Auth/ResetYourPassword/styles.module.scss create mode 100644 src/components/pages/Auth/index.tsx create mode 100644 src/components/pages/Auth/styles.module.scss create mode 100644 src/components/ui/AlreadyHaveAccount/index.tsx create mode 100644 src/components/ui/AlreadyHaveAccount/styles.module.scss diff --git a/src/api/api.ts b/src/api/api.ts index 6a8f99b..8e17533 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -32,7 +32,9 @@ import { UserVideos, UserPDF, Locale, - Session + Session, + Login, + Password } from './resources' const api = { @@ -80,6 +82,8 @@ const api = { getPalmistryLines: createMethod(Palmistry.createRequest), // New Authorization authorization: createMethod(User.createAuthorizeRequest), + login: createMethod(Login.createRequest), + resetPassword: createMethod(Password.resetRequest), // Paywall getPaywallByPlacementKey: createMethod(Paywall.createRequestGet), // Payment diff --git a/src/api/resources/Login.ts b/src/api/resources/Login.ts new file mode 100644 index 0000000..cddab2e --- /dev/null +++ b/src/api/resources/Login.ts @@ -0,0 +1,27 @@ +import routes from "@/routes"; +import { getBaseHeaders } from "../utils"; + +export interface Payload { + email: string; + locale: string; + timezone: string; + password: string; +} + +export type Response = { + status: string, + message: string +} | { + token: string; + userId: string; +} + +export const createRequest = (data: Payload) => { + const url = new URL(routes.server.login()); + const body = JSON.stringify(data); + return new Request(url, { + method: "POST", + body, + headers: getBaseHeaders() + }); +}; diff --git a/src/api/resources/Password.ts b/src/api/resources/Password.ts new file mode 100644 index 0000000..20b1816 --- /dev/null +++ b/src/api/resources/Password.ts @@ -0,0 +1,22 @@ +import routes from "@/routes"; +import { getBaseHeaders } from "../utils"; + +export interface Payload { + email: string; +} + +export type Response = { + status: string; + message: string; + password?: string; +} + +export const resetRequest = (data: Payload) => { + const url = new URL(routes.server.resetPassword()); + const body = JSON.stringify(data); + return new Request(url, { + method: "POST", + body, + headers: getBaseHeaders() + }); +}; diff --git a/src/api/resources/index.ts b/src/api/resources/index.ts index 08b59f0..3e9a2d7 100644 --- a/src/api/resources/index.ts +++ b/src/api/resources/index.ts @@ -31,3 +31,5 @@ export * as UserVideos from "./UserVideos"; export * as UserPDF from "./UserPDF"; export * as Locale from "./Locale"; export * as Session from "./Session"; +export * as Login from "./Login"; +export * as Password from "./Password"; diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index ab99467..d326323 100755 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -58,7 +58,7 @@ import { Asset } from "@/api/resources/Assets"; import PaymentResultPage from "../PaymentPage/results"; import PaymentSuccessPage from "../PaymentPage/results/SuccessPage"; import PaymentFailPage from "../PaymentPage/results/ErrorPage"; -import AuthPage from "../AuthPage"; +// import AuthPage from "../AuthPage"; import AuthResultPage from "../AuthResultPage"; import MagicBallPage from "../pages/MagicBall"; import BestiesHoroscopeResult from "../pages/BestiesHoroscopeResult"; @@ -130,6 +130,7 @@ import AddConsultant from "../palmistry/AdditionalPurchases/pages/AddConsultant" import AddGuides from "../palmistry/AdditionalPurchases/pages/AddGuides"; import SkipTrial from "../palmistry/AdditionalPurchases/pages/SkipTrial"; import { parseQueryParams } from "@/services/url"; +import Auth from "../pages/Auth"; const isProduction = import.meta.env.MODE === "production"; @@ -141,7 +142,10 @@ function App(): JSX.Element { const location = useLocation(); const [isSpecialOfferOpen, setIsSpecialOfferOpen] = useState(false); const [leoApng, setLeoApng] = useState(Error); - const [padLockApng, setPadLockApng] = useState(Error); + // const [ + // padLockApng, + // setPadLockApng, + // ] = useState(Error); const navigate = useNavigate(); const api = useApi(); const dispatch = useDispatch(); @@ -283,13 +287,13 @@ function App(): JSX.Element { getApng(); }, [data]); - useEffect(() => { - (async () => { - const response = await fetch("/padlock_icon_animation_closing.png"); - const arrayBuffer = await response.arrayBuffer(); - setPadLockApng(parseAPNG(arrayBuffer)); - })(); - }, []); + // useEffect(() => { + // (async () => { + // const response = await fetch("/padlock_icon_animation_closing.png"); + // const arrayBuffer = await response.arrayBuffer(); + // setPadLockApng(parseAPNG(arrayBuffer)); + // })(); + // }, []); useEffect(() => { if (!user) return; @@ -308,6 +312,7 @@ function App(): JSX.Element { return ( + } /> } /> } /> } /> @@ -825,10 +830,10 @@ function App(): JSX.Element { path={routes.client.emailEnter()} element={} /> - } - /> + /> */} } diff --git a/src/components/HomePage/index.tsx b/src/components/HomePage/index.tsx index 6869ff7..476465e 100644 --- a/src/components/HomePage/index.tsx +++ b/src/components/HomePage/index.tsx @@ -141,6 +141,9 @@ function HomePage(): JSX.Element { token, key, }); + if (pdf?.status === "not_allowed") { + return pdf; + } if (!pdf?.url?.length || pdf.status !== "ready") { await sleep(5000); return getUserPDF(key); diff --git a/src/components/PalmistryV1/pages/GenderPalmistry/index.tsx b/src/components/PalmistryV1/pages/GenderPalmistry/index.tsx index 3c8bbdc..77a96d0 100644 --- a/src/components/PalmistryV1/pages/GenderPalmistry/index.tsx +++ b/src/components/PalmistryV1/pages/GenderPalmistry/index.tsx @@ -18,6 +18,7 @@ import { usePreloadImages } from "@/hooks/preload/images"; import { ELottieKeys, useLottie } from "@/hooks/lottie/useLottie"; import { useSession } from "@/hooks/session/useSession"; import { EGender, ESourceAuthorization } from "@/api/resources/User"; +import AlreadyHaveAccount from "@/components/ui/AlreadyHaveAccount"; function GenderPalmistry() { const { translate } = useTranslations(ELocalesPlacement.PalmistryV1); @@ -76,6 +77,7 @@ function GenderPalmistry() {

{translate("/gender.description")}

+ {gender && !privacyPolicyChecked && ( diff --git a/src/components/pages/ABDesign/v1/pages/EmailEnterPage/PasswordInput.tsx b/src/components/pages/ABDesign/v1/pages/EmailEnterPage/PasswordInput.tsx new file mode 100644 index 0000000..7a0e34c --- /dev/null +++ b/src/components/pages/ABDesign/v1/pages/EmailEnterPage/PasswordInput.tsx @@ -0,0 +1,51 @@ +import { useState } from "react"; +import styles from "./styles.module.css"; +import { FormField } from "@/types"; + +interface IPasswordInputProps { + value: string; + placeholder: string; + onValid: (value: string) => void; + onInvalid: () => void; +} + +const isValidPassword = (password: string) => { + return !!(password.length >= 6 && password.length < 30); +}; + +function PasswordInput({ + inputClassName, + value, + placeholder, + onValid, + onInvalid, +}: IPasswordInputProps & Partial>) { + const [password, setPassword] = useState(value); + + const handleChange = (event: React.ChangeEvent) => { + const password = event.target.value; + if (!isValidPassword(password)) { + onInvalid(); + } else { + onValid(password); + } + setPassword(password); + }; + + return ( +
+ + {placeholder} +
+ ); +} + +export default PasswordInput; diff --git a/src/components/pages/ABDesign/v1/pages/Gender/index.tsx b/src/components/pages/ABDesign/v1/pages/Gender/index.tsx index bb177c8..32c2956 100644 --- a/src/components/pages/ABDesign/v1/pages/Gender/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/Gender/index.tsx @@ -22,6 +22,7 @@ import { useTranslations } from "@/hooks/translations"; import { ELocalesPlacement } from "@/locales"; import { useSession } from "@/hooks/session/useSession"; import { EGender, ESourceAuthorization } from "@/api/resources/User"; +import AlreadyHaveAccount from "@/components/ui/AlreadyHaveAccount"; interface IGenderPageProps { productKey?: EProductKeys; @@ -208,6 +209,7 @@ function GenderPage({ productKey }: IGenderPageProps): JSX.Element { ))} + {selectedGender && !privacyPolicyChecked && ( diff --git a/src/components/pages/Auth/ResetYourPassword/index.tsx b/src/components/pages/Auth/ResetYourPassword/index.tsx new file mode 100644 index 0000000..7a435a2 --- /dev/null +++ b/src/components/pages/Auth/ResetYourPassword/index.tsx @@ -0,0 +1,40 @@ +import { Dispatch, SetStateAction } from "react"; +import styles from "./styles.module.scss"; +import { ErrorPayload, Password, useApi } from "@/api"; + +interface IResetPasswordProps { + email: string; + setResultResetPassword: Dispatch< + SetStateAction + >; + onClick?: () => void; +} + +function ResetYourPassword({ + email, + setResultResetPassword, + onClick, +}: IResetPasswordProps) { + const api = useApi(); + + const resetPassword = async () => { + if (!email) return; + try { + setResultResetPassword(await api.resetPassword({ email })); + } catch (error: unknown) { + const response = (error as ErrorPayload).responseData; + if (response?.message && !!response?.status) { + setResultResetPassword(response); + } + console.log(error); + } + }; + + return ( + + ); +} + +export default ResetYourPassword; diff --git a/src/components/pages/Auth/ResetYourPassword/styles.module.scss b/src/components/pages/Auth/ResetYourPassword/styles.module.scss new file mode 100644 index 0000000..588fa8c --- /dev/null +++ b/src/components/pages/Auth/ResetYourPassword/styles.module.scss @@ -0,0 +1,12 @@ +.text { + padding: 0; + background: none; + border: none; + line-height: 127%; + font-size: 14px !important; + color: #9974f6 !important; + text-decoration: underline; + margin: 20px auto; + user-select: none; + cursor: pointer; +} diff --git a/src/components/pages/Auth/index.tsx b/src/components/pages/Auth/index.tsx new file mode 100644 index 0000000..490d411 --- /dev/null +++ b/src/components/pages/Auth/index.tsx @@ -0,0 +1,250 @@ +import routes from "@/routes"; +import styles from "./styles.module.scss"; +import { useTranslations } from "@/hooks/translations"; +import { ELocalesPlacement } from "@/locales"; +import { useDispatch, useSelector } from "react-redux"; +import { useNavigate, useParams } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { useDynamicSize } from "@/hooks/useDynamicSize"; +import { useAuthentication } from "@/hooks/authentication/use-authentication"; +import { actions, selectors } from "@/store"; +import { usePaywall } from "@/hooks/paywall/usePaywall"; +import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall"; +import { ELottieKeys, useLottie } from "@/hooks/lottie/useLottie"; +import metricService, { + EGoals, + EMetrics, +} from "@/services/metric/metricService"; +import BackgroundTopBlob from "../ABDesign/v1/ui/BackgroundTopBlob"; +import Title from "@/components/Title"; +import EmailInput from "../ABDesign/v1/pages/EmailEnterPage/EmailInput"; +import QuestionnaireGreenButton from "../ABDesign/v1/ui/GreenButton"; +import Loader, { LoaderColor } from "@/components/Loader"; +import Policy from "@/components/Policy"; +import Header from "../ABDesign/v1/components/Header"; +import PasswordInput from "../ABDesign/v1/pages/EmailEnterPage/PasswordInput"; +import ResetYourPassword from "./ResetYourPassword"; +import Toast from "../ABDesign/v1/components/Toast"; +import { Password } from "@/api"; + +interface IAuthPage { + redirectUrl?: string; +} + +function Auth({ redirectUrl = routes.client.home() }: IAuthPage) { + const { translate } = useTranslations(ELocalesPlacement.V1); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [isDisabled, setIsDisabled] = useState(true); + const [isValidEmail, setIsValidEmail] = useState(false); + const [isValidPassword, setIsValidPassword] = useState(false); + const [isAuth, setIsAuth] = useState(false); + const { subPlan } = useParams(); + const { width: pageWidth, elementRef: pageRef } = useDynamicSize({}); + const { error, isLoading, token, user, authorizationWithPassword } = + useAuthentication(); + const { gender } = useSelector(selectors.selectQuestionnaire); + const activeProductFromStore = useSelector(selectors.selectActiveProduct); + const { products } = usePaywall({ + placementKey: EPlacementKeys["aura.placement.redesign.main"], + localesPlacement: ELocalesPlacement.V1, + }); + const [activeProduct, setActiveProduct] = useState( + activeProductFromStore + ); + const [toastText, setToastText] = useState(""); + const [resultResetPassword, setResultResetPassword] = + useState(); + + useLottie({ + preloadKey: ELottieKeys.handWithStars, + }); + + useEffect(() => { + if (subPlan) { + const targetProduct = products.find( + (product) => + String( + product?.trialPrice + ? Math.floor((product?.trialPrice + 1) / 100) + : product.key.replace(".", "") + ) === subPlan + ); + if (targetProduct) { + setActiveProduct(targetProduct); + } + } + }, [subPlan, products]); + + const handleValidEmail = (email: string) => { + dispatch(actions.form.addEmail(email)); + setEmail(email); + setIsValidEmail(true); + }; + + const handleValidPassword = (password: string) => { + // if (password) { + // dispatch( + // actions.user.update({ + // password, + // }) + // ); + // } + setPassword(password); + setIsValidPassword(true); + }; + + useEffect(() => { + setToastText(""); + if (isValidPassword && isValidEmail) { + setIsDisabled(false); + } else { + setIsDisabled(true); + } + }, [isValidEmail, email, password, isValidPassword]); + + const handleClick = async () => { + authorize(); + metricService.reachGoal(EGoals.ENTERED_EMAIL, [ + EMetrics.KLAVIYO, + EMetrics.YANDEX, + EMetrics.FACEBOOK, + ]); + }; + + const authorize = async () => { + const result = await authorizationWithPassword(email, password); + if (!!result && "message" in result && result?.message?.length) { + setToastText(result.message); + } + }; + + useEffect(() => { + if (user && token?.length && !isLoading && !error) { + dispatch( + actions.payment.update({ + activeProduct, + }) + ); + setIsAuth(true); + dispatch(actions.paywalls.resetIsMustUpdate()); + const timeout = setTimeout(() => { + navigate(redirectUrl); + }, 1000); + return () => { + clearTimeout(timeout); + }; + } + }, [ + activeProduct, + dispatch, + error, + isLoading, + navigate, + redirectUrl, + token?.length, + user, + ]); + + useEffect(() => { + if (resultResetPassword && resultResetPassword.message) { + setToastText(resultResetPassword.message); + } + }, [resultResetPassword]); + + const handleClickResetPassword = () => { + setToastText("Wrong email"); + }; + + return ( +
+ +
+ + {translate("/email.title")} + +

{translate("/email.description")}

+ setIsValidEmail(false)} + /> + setIsValidPassword(false)} + /> + + {isLoading && } + {!isLoading && + !(!error?.length && !isLoading && isAuth) && + translate("continue")} + {!error?.length && !isLoading && isAuth && ( + Success Icon + )} + + + + {translate("/email.policy", { + eulaLink: ( + + {translate("/email.policy_eula")} + + ), + privacyPolicy: ( + + {translate("privacy_policy")} + + ), + })} + + {/* {!!error?.length && ( + + Something went wrong + + )} */} + {!!toastText && ( + + {toastText} + + )} +
+ ); +} + +export default Auth; diff --git a/src/components/pages/Auth/styles.module.scss b/src/components/pages/Auth/styles.module.scss new file mode 100644 index 0000000..81c9cdc --- /dev/null +++ b/src/components/pages/Auth/styles.module.scss @@ -0,0 +1,128 @@ +.page { + height: fit-content; + min-height: 100dvh; + width: 100%; + max-width: 460px; + margin: 0 auto; + padding-bottom: 86px; +} + +.title { + font-size: 27px; + line-height: 125%; + font-weight: 600; + color: #000; + margin-top: 70px; + margin-bottom: 10px; +} + +.not-share { + font-size: 15px; + line-height: 125%; + text-align: center; + max-width: 330px; + margin-left: auto; + margin-right: auto; + margin-bottom: 32px; + color: #000; +} + +.button { + width: 100%; + max-width: 400px; +} + +.policy { + // margin-top: 20px; + max-width: 400px; +} + +.policy > p { + font-size: 15px; + line-height: 125%; +} + +.link { + font-size: 12px !important; + color: #9974f6 !important; +} + +.success-icon { + height: 100%; + width: auto; +} + +.input-container { + width: 100%; + position: relative; + text-align: center; + margin-bottom: 20px; + max-width: 400px; + min-width: 250px; +} + +.input-container > input { + appearance: none; + border-radius: 14px; + color: #121620; + font-size: 15px; + height: 48px; + line-height: 125%; + outline: none; + padding: 16px 24px 5px; + transition: border-color 0.3s ease; + width: 100%; +} + +.input-container > input:focus { + border-color: #000; + transition-delay: 0.1s; +} + +.input-container > input:focus + .input__placeholder, +.input-container > input:not(:placeholder-shown) + .input__placeholder { + font-size: 12px; + top: 12px; + width: auto; +} + +.input__placeholder { + color: #8e8e93; + font-size: 16px; + left: 24px; + overflow: hidden; + text-overflow: ellipsis; + transition: top 0.3s ease, color 0.3s ease, font-size 0.3s ease; + white-space: nowrap; + position: absolute; + top: 50%; + transform: translateY(-50%); + user-select: none; + pointer-events: none; +} + +.background-top-blob { + position: absolute; + top: 0; + left: 0; + scale: 1.4; +} + +.header { + z-index: 3; + width: calc(100% + 36px) !important; +} + +.toast-container { + position: fixed; + bottom: calc(0dvh + 16px); + margin-top: 16px; + max-width: 460px; + padding: 0 24px; + width: 100%; +} + +.toast-text { + text-align: center; + font-size: 16px; +} diff --git a/src/components/ui/AlreadyHaveAccount/index.tsx b/src/components/ui/AlreadyHaveAccount/index.tsx new file mode 100644 index 0000000..83c742a --- /dev/null +++ b/src/components/ui/AlreadyHaveAccount/index.tsx @@ -0,0 +1,18 @@ +import { useNavigate } from "react-router-dom"; +import styles from "./styles.module.scss"; +import routes from "@/routes"; + +function AlreadyHaveAccount() { + const navigate = useNavigate(); + + const navigateAuth = () => { + navigate(routes.client.auth()); + }; + return ( + + ); +} + +export default AlreadyHaveAccount; diff --git a/src/components/ui/AlreadyHaveAccount/styles.module.scss b/src/components/ui/AlreadyHaveAccount/styles.module.scss new file mode 100644 index 0000000..1b7a9c5 --- /dev/null +++ b/src/components/ui/AlreadyHaveAccount/styles.module.scss @@ -0,0 +1,9 @@ +.have-account { + background: none; + border: none; + padding: 0; + margin-top: 20px; + font-size: 14px; + line-height: 140%; + color: rgb(79, 79, 79); +} diff --git a/src/hooks/authentication/use-authentication.ts b/src/hooks/authentication/use-authentication.ts index ce1513f..9b466dd 100644 --- a/src/hooks/authentication/use-authentication.ts +++ b/src/hooks/authentication/use-authentication.ts @@ -1,4 +1,4 @@ -import { useApi } from "@/api"; +import { ErrorPayload, useApi } from "@/api"; import { EGender, ESourceAuthorization, ICreateAuthorizePayload } from "@/api/resources/User"; import { useAuth } from "@/auth"; import { getClientTimezone } from "@/locales"; @@ -10,6 +10,7 @@ import moment from "moment"; import { useCallback, useMemo, useState } from "react"; import { useTranslations } from "@/hooks/translations"; import { useDispatch, useSelector } from "react-redux"; +import { Response } from "@/api/resources/Login"; @@ -118,6 +119,47 @@ export const useAuthentication = () => { dateOfCheck ]); + const authorizationWithPassword = useCallback(async (email: string, password: string) => { + try { + setIsLoading(true); + setError(null) + const payload = { + email, + locale, + timezone: getClientTimezone(), + password + } + const loginResult = await api.login(payload); + const token = "token" in loginResult ? loginResult.token : null; + const userId = "userId" in loginResult ? loginResult.userId : null; + const status = "status" in loginResult ? loginResult.status : null; + const message = "message" in loginResult ? loginResult.message : null; + if (!token) { + return { + status, + message + } + } + const { user } = await api.getUser({ token }); + if (userId?.length) { + metricService.userParams({ + email: user.email, + UserID: userId + }) + metricService.setUserID(userId); + } + signUp(token, user); + setToken(token); + dispatch(actions.status.update("registred")); + } catch (error: unknown) { + const response = (error as ErrorPayload).responseData + setError((!!response && "message" in response && response.message) || (error as Error).message); + return response + } finally { + setIsLoading(false); + } + }, [api, dispatch, locale, signUp]) + const authorization = useCallback(async (email: string, source: ESourceAuthorization) => { try { setIsLoading(true); @@ -156,8 +198,16 @@ export const useAuthentication = () => { error, token, user, - authorization + authorization, + authorizationWithPassword, }), - [isLoading, error, token, user, authorization] + [ + isLoading, + error, + token, + user, + authorization, + authorizationWithPassword, + ] ); } diff --git a/src/routes.ts b/src/routes.ts index 0ccddad..5a09f7d 100755 --- a/src/routes.ts +++ b/src/routes.ts @@ -310,6 +310,10 @@ const routes = { dApiGetRealToken: () => [dApiHost, "users", "auth", "token"].join("/"), + login: () => [dApiHost, "users", "auth", "login"].join("/"), + + resetPassword: () => [dApiHost, "users", "auth", "password"].join("/"), + assistants: () => [apiHost, prefix, "ai", "assistants.json"].join("/"), setExternalChatIdAssistants: (chatId: string) => [apiHost, prefix, "ai", "assistants", chatId, "chats.json"].join("/"), @@ -568,7 +572,7 @@ export const getRouteBy = (status: UserStatus): string => { return routes.client.genderV1(); case "registred": case "unsubscribed": - return routes.client.trialPayment(); + return routes.client.trialPaymentV1(); case "subscribed": return routes.client.home(); default: