Merge branch 'develop'

# Conflicts:
#	src/components/palmistry/step-subscription-plan/step-subscription-plan.tsx
This commit is contained in:
dev.daminik00 2024-05-05 01:04:57 +02:00
commit f4c78ece2e
14 changed files with 344 additions and 167 deletions

14
package-lock.json generated
View File

@ -17,6 +17,7 @@
"html-react-parser": "^3.0.16",
"i18next": "^22.5.0",
"i18next-react-postprocessor": "^3.1.0",
"moment": "^2.30.1",
"react": "^18.2.0",
"react-circular-progressbar": "^2.1.0",
"react-dom": "^18.2.0",
@ -2665,6 +2666,14 @@
"node": "*"
}
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -5381,6 +5390,11 @@
"brace-expansion": "^1.1.7"
}
},
"moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",

View File

@ -23,6 +23,7 @@
"html-react-parser": "^3.0.16",
"i18next": "^22.5.0",
"i18next-react-postprocessor": "^3.1.0",
"moment": "^2.30.1",
"react": "^18.2.0",
"react-circular-progressbar": "^2.1.0",
"react-dom": "^18.2.0",

View File

@ -34,6 +34,7 @@ const api = {
auth: createMethod<AuthTokens.Payload, AuthTokens.Response>(AuthTokens.createRequest),
appleAuth: createMethod<AppleAuth.Payload, AppleAuth.Response>(AppleAuth.createRequest),
googleAuth: createMethod<GoogleAuth.Payload, GoogleAuth.Response>(GoogleAuth.createRequest),
getRealToken: createMethod<AuthTokens.PayloadGetRealToken, AuthTokens.ResponseGetRealToken>(AuthTokens.createGetRealTokenRequest),
getAppConfig: createMethod<Apps.Payload, Apps.Response>(Apps.createRequest),
getElement: createMethod<Element.Payload, Element.Response>(Element.createRequest),
getElements: createMethod<Elements.Payload, Elements.Response>(Elements.createRequest),
@ -72,6 +73,8 @@ const api = {
createSinglePayment: createMethod<SinglePayment.PayloadPost, SinglePayment.ResponsePost>(SinglePayment.createRequestPost),
checkProductPurchased: createMethod<Products.PayloadGet, Products.ResponseGet>(Products.createRequest),
getPalmistryLines: createMethod<Palmistry.Payload, Palmistry.Response>(Palmistry.createRequest),
// New Authorization
authorization: createMethod<User.ICreateAuthorizePayload, User.ICreateAuthorizeResponse>(User.createAuthorizeRequest),
}
export type ApiContextValue = typeof api

View File

@ -1,7 +1,7 @@
import routes from "@/routes";
import { AuthToken } from "../types";
import { User } from "./User";
import { getBaseHeaders } from "../utils";
import { getAuthHeaders, getBaseHeaders } from "../utils";
export interface PayloadRegisterByEmail {
email: string;
@ -41,3 +41,16 @@ export const createRequest = (payload: Payload): Request => {
const body = JSON.stringify({ auth: { ...payload } });
return new Request(url, { method: "POST", headers: getBaseHeaders(), body });
};
export interface PayloadGetRealToken {
token: string;
}
export interface ResponseGetRealToken {
token: string;
}
export const createGetRealTokenRequest = ({ token }: PayloadGetRealToken): Request => {
const url = new URL(routes.server.dApiGetRealToken());
return new Request(url, { method: "POST", headers: getAuthHeaders(token) });
}

View File

@ -1,6 +1,6 @@
import routes from "@/routes";
import { AuthPayload } from "../types";
import { getAuthHeaders } from "../utils";
import { getAuthHeaders, getBaseHeaders } from "../utils";
export type GetPayload = AuthPayload;
@ -130,3 +130,57 @@ export const createPatchRequest = ({ token, user }: PatchPayload): Request => {
body,
});
};
export enum ESourceAuthorization {
"aura.main" = "aura.main",
"aura.palmistry" = "aura.palmistry",
"aura.chat" = "aura.chat",
"aura.moons" = "aura.moons"
}
export enum EGender {
"male" = "male",
"female" = "female",
"other" = "other"
}
enum ERelationshipStatus {
"single",
"relationship",
"married",
"complicated",
"other"
}
interface ICreateAuthorizeUser {
name: string;
birthdate: string | null;
gender: EGender;
birthplace: {
address?: string;
coords?: string;
}
relationship_status: ERelationshipStatus;
}
export interface ICreateAuthorizePayload {
email: string;
locale: string;
timezone: string;
source: ESourceAuthorization;
profile?: Partial<ICreateAuthorizeUser>;
partner?: Partial<Exclude<ICreateAuthorizeUser, "relationship_status">>;
}
export interface ICreateAuthorizeResponse {
token: string;
}
export const createAuthorizeRequest = (data: ICreateAuthorizePayload): Request => {
const body = JSON.stringify(data);
return new Request(routes.server.dApiAuth(), {
method: "POST",
headers: getBaseHeaders(),
body,
});
}

View File

@ -198,11 +198,14 @@ function App(): JSX.Element {
(async () => {
if (!jwtToken) return;
logout();
const auth = await api.auth({ jwt: jwtToken });
const {
auth: { token, user },
} = auth;
signUp(token, user);
try {
const { token } = await api.getRealToken({ token: jwtToken });
const { user } = await api.getUser({ token });
signUp(token, user);
} catch (error) {
console.log("Error of get real token or get user: ");
console.error(error);
}
})();
}, [api, jwtToken, logout, signUp]);
@ -318,6 +321,9 @@ function App(): JSX.Element {
no: routes.client.epeGender(),
force: routes.client.epeBirthdate(),
},
purchasedProduct: {
no: routes.client.epePayment(),
},
}}
requiredParameters={[birthdate, isForceShortPath || gender]}
/>
@ -487,6 +493,9 @@ function App(): JSX.Element {
no: routes.client.advisorChatGender(),
force: routes.client.advisorChatBirthdate(),
},
purchasedProduct: {
no: routes.client.advisorChatPayment(),
},
}}
requiredParameters={[
birthdate,
@ -1075,6 +1084,7 @@ interface IShortPathOutletProps {
function ShortPathOutlet(props: IShortPathOutletProps): JSX.Element {
const { productKey, requiredParameters, redirectUrls, isProductPage } = props;
const { user, token } = useAuth();
const api = useApi();
const isForce = useSelector(selectors.selectIsForceShortPath);

View File

@ -4,18 +4,17 @@ import { useNavigate, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store";
import { getClientTimezone } from "@/locales";
import { useAuth } from "@/auth";
import { useApi, ApiError, extractErrorMessage } from "@/api";
import { useApi } from "@/api";
import Title from "../Title";
import Policy from "../Policy";
import EmailInput from "./EmailInput";
import MainButton from "../MainButton";
import Loader, { LoaderColor } from "../Loader";
import ErrorText from "../ErrorText";
import routes from "@/routes";
import NameInput from "./NameInput";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import { useAuthentication } from "@/hooks/authentication/use-authentication";
import { ESourceAuthorization } from "@/api/resources/User";
interface IEmailEnterPage {
redirectUrl?: string;
@ -27,33 +26,23 @@ function EmailEnterPage({
isRequiredName = false,
}: IEmailEnterPage): JSX.Element {
const api = useApi();
const { signUp } = useAuth();
const { t, i18n } = useTranslation();
const dispatch = useDispatch();
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [name, setName] = useState("");
const birthday = useSelector(selectors.selectBirthday);
const [isDisabled, setIsDisabled] = useState(true);
const [isValidEmail, setIsValidEmail] = useState(false);
const [isValidName, setIsValidName] = useState(!isRequiredName);
const [isLoading, setIsLoading] = useState(false);
const [isAuth, setIsAuth] = useState(false);
const [apiError, setApiError] = useState<ApiError | null>(null);
const [error, setError] = useState<boolean>(false);
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan);
const [activeSubPlan, setActiveSubPlan] = useState<ISubscriptionPlan | null>(
activeSubPlanFromStore
);
const timezone = getClientTimezone();
const locale = i18n.language;
const { subPlan } = useParams();
const {
gender,
birthPlace,
// flowChoice
} = useSelector(selectors.selectQuestionnaire);
const { error, isLoading, authorization } = useAuthentication();
useEffect(() => {
if (subPlan) {
@ -99,6 +88,13 @@ function EmailEnterPage({
};
const handleValidName = (name: string) => {
if (name) {
dispatch(
actions.user.update({
username: name,
})
);
}
setName(name);
setIsValidName(true);
};
@ -112,62 +108,27 @@ function EmailEnterPage({
}, [isValidEmail, isValidName, email, name]);
const handleClick = () => {
authorization();
authorize();
};
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,
gender: gender.length ? gender : "male",
full_name: name,
// relationship_status: !!flowChoice.length ? flowChoice : null,
},
birthplace_attributes: { address: birthPlace },
},
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"));
dispatch(
actions.payment.update({
activeSubPlan,
})
);
setIsLoading(false);
setIsAuth(true);
setTimeout(() => {
navigate(redirectUrl);
}, 1000);
} catch (error) {
console.error(error);
if (error instanceof ApiError) {
setApiError(error as ApiError);
} else {
setError(true);
}
setIsLoading(false);
const authorize = async () => {
let source = ESourceAuthorization["aura.main"];
if (window.location.pathname.includes("advisor-chat")) {
source = ESourceAuthorization["aura.chat"];
}
if (window.location.pathname.includes("/epe/")) {
source = ESourceAuthorization["aura.moons"];
}
await authorization(email, source);
dispatch(
actions.payment.update({
activeSubPlan,
})
);
setIsAuth(true);
setTimeout(() => {
navigate(redirectUrl);
}, 1000);
};
return (
@ -220,9 +181,9 @@ function EmailEnterPage({
>
{isLoading && <Loader color={LoaderColor.White} />}
{!isLoading &&
!(!apiError && !error && !isLoading && isAuth) &&
!(!error?.length && !isLoading && isAuth) &&
t("_continue")}
{!apiError && !error && !isLoading && isAuth && (
{!error?.length && !isLoading && isAuth && (
<img
className={styles["success-icon"]}
src="/SuccessIcon.png"
@ -230,18 +191,11 @@ function EmailEnterPage({
/>
)}
</MainButton>
{(error || apiError) && (
{!!error?.length && (
<Title variant="h3" style={{ color: "red", margin: 0 }}>
Something went wrong
</Title>
)}
{apiError && (
<ErrorText
size="medium"
isShown={Boolean(apiError)}
message={apiError ? extractErrorMessage(apiError) : null}
/>
)}
</section>
);
}

View File

@ -26,7 +26,7 @@ function TrialChoicePage() {
const email = useSelector(selectors.selectEmail);
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
const [isDisabled, setIsDisabled] = useState(true);
const allowedPlans = useMemo(() => ["stripe.37"], []);
const allowedPlans = useMemo(() => [""], []);
useEffect(() => {
(async () => {

View File

@ -1,38 +1,27 @@
import React from 'react';
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";
import useSteps from '@/hooks/palmistry/use-steps';
import Button from '@/components/palmistry/button/button';
import Input from '@/components/palmistry/input/input';
import { getClientTimezone } from "@/locales";
import useSteps from "@/hooks/palmistry/use-steps";
import Button from "@/components/palmistry/button/button";
import Input from "@/components/palmistry/input/input";
import { actions } from "@/store";
import Title from "@/components/Title";
import ErrorText from "@/components/ErrorText";
import Loader, { LoaderColor } from "@/components/Loader";
import { useAuthentication } from "@/hooks/authentication/use-authentication";
import { ESourceAuthorization } from "@/api/resources/User";
const emailRegex = /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/;
export default function StepEmail() {
const api = useApi();
const { signUp } = useAuth();
const { t, i18n } = useTranslation();
const { t } = useTranslation();
const dispatch = useDispatch();
const steps = useSteps();
const [email, setEmail] = React.useState(steps.storedValue);
const [emailIsValid, setEmailIsValid] = React.useState(false);
const [isLoading, setIsLoading] = React.useState(false);
const [isAuth, setIsAuth] = React.useState(false);
const [apiError, setApiError] = React.useState<ApiError | null>(null);
const [error, setError] = React.useState<boolean>(false);
const timezone = getClientTimezone();
const locale = i18n.language;
const { error, isLoading, authorization } = useAuthentication();
const onChangeEmail = (value: string) => {
setEmail(value);
@ -49,51 +38,16 @@ export default function StepEmail() {
if (emailIsValid) {
dispatch(actions.form.addEmail(email));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [email]);
const authorization = async () => {
try {
setIsLoading(true);
const auth = await api.auth({ email, timezone, locale });
const { auth: { token, user } } = auth;
signUp(token, user);
const payload: PatchPayload = {
user: {
profile_attributes: {
birthday: steps.getStoredValue(Step.Birthdate),
gender: steps.getStoredValue(Step.Gender),
},
},
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));
dispatch(actions.status.update("registred"));
setIsLoading(false);
setIsAuth(true);
} catch (error) {
console.error(error);
if (error instanceof ApiError) {
setApiError(error as ApiError);
} else {
setError(true);
}
setIsLoading(false);
}
const authorize = async () => {
await authorization(email, ESourceAuthorization["aura.palmistry"]);
setIsAuth(true);
};
const onNext = async () => {
await authorization();
await authorize();
steps.saveCurrent(email);
steps.goNext();
};
@ -101,7 +55,9 @@ export default function StepEmail() {
return (
<>
<div className="palmistry-container__title-wrapper">
<h2>Enter your email to get your advanced Palmistry reading with AURA</h2>
<h2>
Enter your email to get your advanced Palmistry reading with AURA
</h2>
</div>
<Input
@ -119,12 +75,20 @@ export default function StepEmail() {
<div className="palmistry-container__policy">
<p>
By clicking "Continue" below you agree to AURA{' '}
<a href="https://aura.wit.life/terms" target="_blank" rel="noreferrer nofollow">
By clicking "Continue" below you agree to AURA{" "}
<a
href="https://aura.wit.life/terms"
target="_blank"
rel="noreferrer nofollow"
>
EULA
</a>{' '}
and{' '}
<a href="https://aura.wit.life/privacy" target="_blank" rel="noreferrer nofollow">
</a>{" "}
and{" "}
<a
href="https://aura.wit.life/privacy"
target="_blank"
rel="noreferrer nofollow"
>
Privacy Policy
</a>
.
@ -140,9 +104,11 @@ export default function StepEmail() {
>
{isLoading && <Loader color={LoaderColor.White} />}
{!isLoading && !(!apiError && !error && !isLoading && isAuth) && t("_continue")}
{!isLoading &&
!(!error?.length && !isLoading && isAuth) &&
t("_continue")}
{!apiError && !error && !isLoading && isAuth && (
{!error?.length && !isLoading && isAuth && (
<img
style={{ height: "30px", width: "auto" }}
src="/SuccessIcon.png"
@ -153,19 +119,11 @@ export default function StepEmail() {
{isLoading && <Loader color={LoaderColor.White} />}
{(error || apiError) && (
{error?.length && (
<Title variant="h3" style={{ color: "red", margin: 0 }}>
Something went wrong
</Title>
)}
{apiError && (
<ErrorText
size="medium"
isShown={Boolean(apiError)}
message={apiError ? extractErrorMessage(apiError) : null}
/>
)}
</>
);
}

View File

@ -1,10 +1,14 @@
import Button from '../button/button';
import useSteps, { GenderChoice } from '../../../hooks/palmistry/use-steps';
import Button from "../button/button";
import useSteps, { GenderChoice } from "../../../hooks/palmistry/use-steps";
import { useDispatch } from "react-redux";
import { actions } from "@/store";
export default function StepGender() {
const steps = useSteps();
const dispatch = useDispatch();
const onNext = (choice: GenderChoice) => {
dispatch(actions.questionnaire.update({ gender: choice }));
steps.saveCurrent(choice);
steps.goNext(choice);
};
@ -14,9 +18,10 @@ export default function StepGender() {
<h3 className="palmistry-container__header">Whats your gender?</h3>
<p className="palmistry-container__description">
In Palmistry, everyone is a blend of masculine and feminine, so it helps to know yours.
In Palmistry, everyone is a blend of masculine and feminine, so it helps
to know yours.
</p>
<div className="palmistry-container__button-wrapper">
<Button
type="button"

View File

@ -23,7 +23,7 @@ export default function StepSubscriptionPlan() {
const api = useApi();
const { i18n } = useTranslation();
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan);
const allowedPlans = useMemo(() => ["stripe.37"], []);
const allowedPlans = useMemo(() => [""], []);
const storedEmail = steps.getStoredValue(Step.Email);

View File

@ -0,0 +1,140 @@
import { useApi } from "@/api";
import { EGender, ESourceAuthorization, ICreateAuthorizePayload } from "@/api/resources/User";
import { useAuth } from "@/auth";
import { getClientTimezone } from "@/locales";
import { getDateAsString } from "@/services/date";
import { filterNullKeysOfObject } from "@/services/filter-object";
import { actions, selectors } from "@/store";
import moment from "moment";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
export const useAuthentication = () => {
const api = useApi();
const { i18n } = useTranslation();
const dispatch = useDispatch();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [token, setToken] = useState<string | null>(null);
const { user, signUp } = useAuth();
const locale = i18n.language;
const { username } = useSelector(selectors.selectUser)
const { name: partnerName, birthDate: partnerBirthdateFromForm } = useSelector(selectors.selectRightUser)
const {
gender,
birthdate: birthdateFromQuestionnaire,
birthtime: birthtimeFromQuestionnaire,
birthPlace,
partnerGender,
partnerBirthdate: partnerBirthdateFromQuestionnaire,
partnerBirthPlace,
partnerBirthtime,
} = useSelector(selectors.selectQuestionnaire)
const birthdateFromForm = useSelector(selectors.selectBirthdate);
const birthtimeFromForm = useSelector(selectors.selectBirthtime);
const birthdate = useMemo(() => {
if (birthdateFromQuestionnaire.length) {
return birthdateFromQuestionnaire;
}
if (birthdateFromForm.length) {
return birthdateFromForm;
}
}, [birthdateFromForm, birthdateFromQuestionnaire]);
const birthtime = useMemo(() => {
if (birthtimeFromQuestionnaire.length) {
return birthtimeFromQuestionnaire;
}
if (birthtimeFromForm.length) {
return birthtimeFromForm;
}
}, [birthtimeFromForm, birthtimeFromQuestionnaire]);
const partnerBirthdate = useMemo(() => {
const fromQuestionnaire = `${partnerBirthdateFromQuestionnaire} ${partnerBirthtime}`
if (partnerBirthdateFromQuestionnaire.length) {
return fromQuestionnaire;
}
return getDateAsString(partnerBirthdateFromForm)
}, [partnerBirthdateFromForm, partnerBirthdateFromQuestionnaire, partnerBirthtime])
const formatDate = useCallback((date: string) => {
const _date = moment(date).format("YYYY-MM-DD HH:mm");
if (_date === "Invalid date") {
return null;
}
return _date;
}, [])
const getAuthorizationPayload = useCallback((email: string, source: ESourceAuthorization): ICreateAuthorizePayload => {
const timezone = getClientTimezone();
return filterNullKeysOfObject<ICreateAuthorizePayload>({
timezone,
locale,
email,
source,
profile: {
name: username || "",
gender: EGender[gender as keyof typeof EGender] || null,
birthdate: formatDate(`${birthdate} ${birthtime}`),
birthplace: {
address: birthPlace,
},
},
partner: {
name: partnerName,
gender: EGender[partnerGender as keyof typeof EGender] || null,
birthdate: formatDate(partnerBirthdate),
birthplace: {
address: partnerBirthPlace,
},
}
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
birthPlace,
birthdate,
gender,
locale,
partnerBirthPlace,
partnerBirthdate,
partnerGender,
partnerName,
username,
birthtime,
]);
const authorization = useCallback(async (email: string, source: ESourceAuthorization) => {
try {
setIsLoading(true);
const payload = getAuthorizationPayload(email, source);
const { token } = await api.authorization(payload);
const { user } = await api.getUser({ token });
signUp(token, user);
setToken(token);
dispatch(actions.status.update("registred"));
} catch (error) {
setError((error as Error).message);
} finally {
setIsLoading(false);
}
}, [api, dispatch, getAuthorizationPayload, signUp])
return useMemo(
() => ({
isLoading,
error,
token,
user,
authorization
}),
[isLoading, error, token, user, authorization]
);
}

View File

@ -215,6 +215,9 @@ const routes = {
[dApiHost, "payment", "products", `${productKey}?email=${email}`].join(
"/"
),
dApiAuth: () => [dApiHost, "users", "auth"].join("/"),
dApiGetRealToken: () => [dApiHost, "users", "auth", "token"].join("/"),
assistants: () => [apiHost, prefix, "ai", "assistants.json"].join("/"),
setExternalChatIdAssistants: (chatId: string) =>

View File

@ -0,0 +1,22 @@
export function filterNullKeysOfObject<T extends object>(object: T): T {
if (typeof object !== "object") {
return object
}
return Object.keys(object)
.filter(key => {
if (typeof object[key as keyof T] === "object" && object[key as keyof T] !== null) {
return Object.keys(object[key as keyof T] as object).length
}
if (typeof object[key as keyof T] === "string") {
return !!(object[key as keyof T] as string).length
}
return object[key as keyof T] !== null
})
.reduce((acc, key) => {
return Object.assign(acc, {
[key]: typeof object[key as keyof T] === "object" ? filterNullKeysOfObject<typeof object>(object[key as keyof object]) : object[key as keyof T]
});
}, Array.isArray(object) ? [] : {}) as T;
}