Merge branch 'develop' into 'main'

Develop

See merge request witapp/aura-webapp!427
This commit is contained in:
Daniil Chemerkin 2024-10-14 15:12:22 +00:00
commit 27808d8fbd
17 changed files with 645 additions and 16 deletions

View File

@ -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.Payload, Palmistry.Response>(Palmistry.createRequest),
// New Authorization
authorization: createMethod<User.ICreateAuthorizePayload, User.ICreateAuthorizeResponse>(User.createAuthorizeRequest),
login: createMethod<Login.Payload, Login.Response>(Login.createRequest),
resetPassword: createMethod<Password.Payload, Password.Response>(Password.resetRequest),
// Paywall
getPaywallByPlacementKey: createMethod<Paywall.PayloadGet, Paywall.ResponseGet>(Paywall.createRequestGet),
// Payment

View File

@ -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()
});
};

View File

@ -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()
});
};

View File

@ -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";

View File

@ -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<boolean>(false);
const [leoApng, setLeoApng] = useState<Error | APNG>(Error);
const [padLockApng, setPadLockApng] = useState<Error | APNG>(Error);
// const [
// padLockApng,
// setPadLockApng,
// ] = useState<Error | APNG>(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 (
<Routes>
<Route path={routes.client.auth()} element={<Auth />} />
<Route path="*" element={<ABDesignV1Routes />} />
<Route path={`${palmistryV1Prefix}/*`} element={<PalmistryV1Routes />} />
<Route path={`${routes.client.mikeV1()}/*`} element={<MikeV1Routes />} />
@ -825,10 +830,10 @@ function App(): JSX.Element {
path={routes.client.emailEnter()}
element={<EmailEnterPage />}
/>
<Route
{/* <Route
path={routes.client.auth()}
element={<AuthPage padLockApng={padLockApng} />}
/>
/> */}
<Route
path={routes.client.authResult()}
element={<AuthResultPage />}

View File

@ -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);

View File

@ -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() {
</Title>
<p className={styles.description}>{translate("/gender.description")}</p>
<ChooseGender onSelectGender={selectGender} />
<AlreadyHaveAccount />
<PrivacyPolicy containerClassName={styles["privacy-policy"]} />
{gender && !privacyPolicyChecked && (
<Toast classNameContainer={styles["toast-container"]} variant="error">

View File

@ -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<FormField<string>>) {
const [password, setPassword] = useState(value);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const password = event.target.value;
if (!isValidPassword(password)) {
onInvalid();
} else {
onValid(password);
}
setPassword(password);
};
return (
<div className={styles["input-container"]}>
<input
className={inputClassName}
type="password"
name="password"
id="password"
value={password}
onChange={handleChange}
placeholder=" "
/>
<span className={styles["input__placeholder"]}>{placeholder}</span>
</div>
);
}
export default PasswordInput;

View File

@ -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 {
</div>
))}
</div>
<AlreadyHaveAccount />
<PrivacyPolicy containerClassName={styles["privacy-policy"]} />
{selectedGender && !privacyPolicyChecked && (
<Toast classNameContainer={styles["toast-container"]} variant="error">

View File

@ -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<Password.Response | undefined>
>;
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<Password.Response>).responseData;
if (response?.message && !!response?.status) {
setResultResetPassword(response);
}
console.log(error);
}
};
return (
<button className={styles.text} onClick={onClick ? onClick : resetPassword}>
Reset your password
</button>
);
}
export default ResetYourPassword;

View File

@ -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;
}

View File

@ -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<IPaywallProduct | null>(
activeProductFromStore
);
const [toastText, setToastText] = useState("");
const [resultResetPassword, setResultResetPassword] =
useState<Password.Response>();
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 (
<section
className={`${styles.page} page`}
ref={pageRef}
style={{ backgroundColor: gender === "male" ? "#C1E5FF" : "#f7ebff" }}
>
<BackgroundTopBlob
width={pageWidth}
className={styles["background-top-blob"]}
height={180}
/>
<Header className={styles.header} />
<Title variant="h2" className={styles.title}>
{translate("/email.title")}
</Title>
<p className={styles["not-share"]}>{translate("/email.description")}</p>
<EmailInput
name="email"
value={email}
placeholder={translate("/email.placeholder_email")}
onValid={handleValidEmail}
onInvalid={() => setIsValidEmail(false)}
/>
<PasswordInput
value={password}
placeholder={"Password"}
onValid={handleValidPassword}
onInvalid={() => setIsValidPassword(false)}
/>
<QuestionnaireGreenButton
className={styles.button}
onClick={handleClick}
disabled={isDisabled}
>
{isLoading && <Loader color={LoaderColor.White} />}
{!isLoading &&
!(!error?.length && !isLoading && isAuth) &&
translate("continue")}
{!error?.length && !isLoading && isAuth && (
<img
className={styles["success-icon"]}
src="/SuccessIcon-white.svg"
alt="Success Icon"
/>
)}
</QuestionnaireGreenButton>
<ResetYourPassword
email={email}
setResultResetPassword={setResultResetPassword}
onClick={!isValidEmail ? handleClickResetPassword : undefined}
/>
<Policy sizing="medium" className={styles.policy}>
{translate("/email.policy", {
eulaLink: (
<a
className={styles.link}
href="https://aura.wit.life/terms"
target="_blank"
rel="noopener noreferrer"
>
{translate("/email.policy_eula")}
</a>
),
privacyPolicy: (
<a
className={styles.link}
href="https://aura.wit.life/privacy"
target="_blank"
rel="noopener noreferrer"
>
{translate("privacy_policy")}
</a>
),
})}
</Policy>
{/* {!!error?.length && (
<Title variant="h3" style={{ color: "red", margin: 0 }}>
Something went wrong
</Title>
)} */}
{!!toastText && (
<Toast classNameContainer={styles["toast-container"]} variant="error">
<span className={styles["toast-text"]}>{toastText}</span>
</Toast>
)}
</section>
);
}
export default Auth;

View File

@ -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;
}

View File

@ -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 (
<button className={styles["have-account"]} onClick={navigateAuth}>
Already have an account? Sign in
</button>
);
}
export default AlreadyHaveAccount;

View File

@ -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);
}

View File

@ -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<Response>).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,
]
);
}

View File

@ -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: