diff --git a/public/v1/email-marketing/circular-text.png b/public/v1/email-marketing/circular-text.png new file mode 100644 index 0000000..8605ac0 Binary files /dev/null and b/public/v1/email-marketing/circular-text.png differ diff --git a/src/api/api.ts b/src/api/api.ts index cc20704..95e68d5 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -41,6 +41,7 @@ const api = { // getElement: createMethod(Element.createRequest), getElements: createMethod(Elements.createRequest), getUser: createMethod(User.createGetRequest), + getMe: createMethod(User.createMeRequest), updateUser: createMethod(User.createPatchRequest), getAssets: createMethod(Assets.createRequest), getAssetCategories: createMethod(AssetCategories.createRequest), @@ -83,6 +84,7 @@ const api = { // Payment makePayment: createMethod(Payment.createRequestPost), getPaymentConfig: createMethod(Payment.getConfigRequest), + getPaymentMethods: createMethod(Payment.getMethodsRequest), // User videos getUserVideos: createMethod(UserVideos.createRequest), // User PDF diff --git a/src/api/resources/Payment.ts b/src/api/resources/Payment.ts index 21effa3..64b95b8 100644 --- a/src/api/resources/Payment.ts +++ b/src/api/resources/Payment.ts @@ -1,7 +1,7 @@ import routes from "@/routes"; import { getAuthHeaders, getBaseHeaders } from "../utils"; -interface Payload { +export interface Payload { token: string; } @@ -33,12 +33,19 @@ interface ResponsePostSuccess { } } +interface ResponsePostSinglePaymentSuccess { + payment: { + status: string; + invoiceId: string; + }; +} + interface ResponsePostError { status: string; message: string; } -export type ResponsePost = ResponsePostSuccess | ResponsePostError; +export type ResponsePost = ResponsePostSuccess | ResponsePostSinglePaymentSuccess | ResponsePostError; export const createRequestPost = ({ token, productId, placementId, paywallId, paymentToken }: PayloadPost): Request => { const url = new URL(routes.server.makePayment()); @@ -64,3 +71,13 @@ export const getConfigRequest = (): Request => { const url = new URL(routes.server.getPaymentConfig()); return new Request(url, { method: "GET", headers: getBaseHeaders() }); }; + +export interface IPaymentMethodsResponse { + status: "success" | "error", + message: string, +} + +export const getMethodsRequest = ({ token }: Payload): Request => { + const url = new URL(routes.server.getPaymentMethods()); + return new Request(url, { method: "GET", headers: getAuthHeaders(token) }); +}; diff --git a/src/api/resources/User.ts b/src/api/resources/User.ts index a9332ea..39b1af1 100644 --- a/src/api/resources/User.ts +++ b/src/api/resources/User.ts @@ -194,3 +194,57 @@ export const createAuthorizeRequest = (data: ICreateAuthorizePayload): Request = body, }); } + +export interface IMeResponse { + user: IUser; +} + +export interface IUser { + ipLookup?: { + country: string; + region: string; + eu: string; + timezone: string; + city: string; + }, + profile: { + birthplace: { + address: string; + }, + name: string; + birthdate: string; + gender: string; + age: number; + sign: string; + }, + partner?: { + birthplace: { + address: string; + }, + birthdate: string; + gender: string; + age: number; + sign: string; + }, + _id: string; + initialIp?: string; + sessionId?: string; + email: string; + locale: string; + timezone: string; + source: string; + sign: boolean; + signDate: string; + password: string; + externalId: string; + klaviyoId: string; + stripeId: string | null; + assistants: string[]; + createdAt: string; + updatedAt: string; +} + +export const createMeRequest = ({ token }: GetPayload): Request => { + const url = new URL(routes.server.me()); + return new Request(url, { method: "GET", headers: getAuthHeaders(token) }); +} diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index d576797..386a128 100755 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -103,7 +103,6 @@ import AdditionalDiscount from "../pages/AdditionalDiscount"; import TrialPaymentWithDiscount from "../pages/TrialPaymentWithDiscount"; import MarketingLanding from "../pages/EmailLetters/MarketingLanding"; import MarketingTrialPayment from "../pages/EmailLetters/MarketingTrialPayment"; -import { ScrollToTop } from "@/hooks/scrollToTop"; import { EUserDeviceType } from "@/store/userConfig"; import TryAppPage from "../pages/TryApp"; import AdditionalPurchases from "../pages/AdditionalPurchases"; @@ -135,6 +134,7 @@ import ChatsRoutes from "@/routerComponents/Chats"; import CookieYesController from "@/routerComponents/CookieYesController"; import PalmistryV2Routes from "@/routerComponents/Palmistry/v2"; import MarketingLandingV1Routes from "@/routerComponents/MarketingLanding/v1"; +import { useScrollToTop } from "@/hooks/useScrollToTop"; const isProduction = import.meta.env.MODE === "production"; @@ -145,6 +145,7 @@ if (isProduction) { function App(): JSX.Element { const location = useLocation(); const [leoApng, setLeoApng] = useState(Error); + useScrollToTop({ scrollBehavior: "auto" }); // const [ // padLockApng, // setPadLockApng, @@ -240,7 +241,22 @@ function App(): JSX.Element { try { const { token } = await api.getRealToken({ token: jwtToken }); const { user } = await api.getUser({ token }); + const { user: userMe } = await api.getMe({ token }); signUp(token, user); + + dispatch(actions.questionnaire.update({ + gender: userMe.profile.gender ?? undefined, + birthPlace: userMe.profile.birthplace?.address ?? undefined, + birthdate: userMe.profile.birthdate ?? undefined, + partnerBirthPlace: userMe.partner?.birthplace?.address ?? undefined, + partnerBirthdate: userMe.partner?.birthdate ?? undefined, + partnerGender: userMe.partner?.gender ?? undefined, + })) + + dispatch(actions.user.update({ + username: userMe.profile.name ?? undefined, + })); + } catch (error) { console.log("Error of get real token or get user: "); console.error(error); @@ -1053,14 +1069,13 @@ function Layout(): JSX.Element { return (
- {showHeader ? (
setIsMenuOpen(true)} /> ) : null} {isRouteFullDataModal && ( - + { }}> )} @@ -1250,7 +1265,7 @@ export function PrivateOutlet(): JSX.Element { function PrivateSubscriptionOutlet(): JSX.Element { const isProduction = import.meta.env.MODE === "production"; const status = useSelector(selectors.selectStatus); - return status === "subscribed" || !isProduction || true ? ( + return status === "subscribed" || !isProduction ? ( ) : ( diff --git a/src/components/ChatsPath/pages/ExpertChat/index.tsx b/src/components/ChatsPath/pages/ExpertChat/index.tsx index c7799ef..3d955f7 100644 --- a/src/components/ChatsPath/pages/ExpertChat/index.tsx +++ b/src/components/ChatsPath/pages/ExpertChat/index.tsx @@ -19,19 +19,20 @@ import { Products, useApi, useApiCall } from "@/api"; import { usePaywall } from "@/hooks/paywall/usePaywall"; import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall"; import { useAuth } from "@/auth"; -import { ResponsePost } from "@/api/resources/SinglePayment"; import { createSinglePayment } from "@/services/singlePayment"; import Modal from "@/components/Modal"; import Title from "@/components/Title"; -import PaymentForm from "@/components/pages/SinglePaymentPage/PaymentForm"; +// import PaymentForm from "@/components/pages/SinglePaymentPage/PaymentForm"; import { getPriceCentsToDollars } from "@/services/price"; import { IMessage } from "@/api/resources/ChatMessages"; import { useTranslations } from "@/hooks/translations"; import { ELocalesPlacement } from "@/locales"; +import PaymentForm from "@/components/Payment/nmi/PaymentForm"; -const returnUrl = `${window.location.protocol}//${ - window.location.host -}${routes.client.chatsExpert()}`; +const returnUrl = `${window.location.protocol}//${window.location.host + }${routes.client.chatsExpert()}`; + +const placementKey = EPlacementKeys["aura.placement.chat"]; function ExpertChat() { const { translate } = useTranslations(ELocalesPlacement.Chats); @@ -66,12 +67,16 @@ function ExpertChat() { // Payment const { user: userFromStore } = useAuth(); const tokenFromStore = useSelector(selectors.selectToken); - const [paymentIntent, setPaymentIntent] = useState(null); const [isLoadingPayment, setIsLoadingPayment] = useState(false); const [isError, setIsError] = useState(false); - const [currentProduct, setCurrentProduct] = useState( - null - ); + // const [currentProduct, setCurrentProduct] = useState( + // null + // ); + const currentProduct = useSelector(selectors.selectActiveProduct); + const setCurrentProduct = (product: IPaywallProduct) => { + dispatch(actions.payment.update({ activeProduct: product })); + }; + const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false); const isPayedFirstPurchase = useSelector( selectors.selectIsPayedFirstPurchase @@ -101,7 +106,7 @@ function ExpertChat() { >(checkIsPayedFirstPurchase); const { products } = usePaywall({ - placementKey: EPlacementKeys["aura.placement.chat"], + placementKey, }); const scrollToBottom = () => { @@ -182,6 +187,13 @@ function ExpertChat() { if (!currentProduct) return; setCurrentProduct(currentProduct); setIsLoadingPayment(true); + const isPaymentMethodExist = await api.getPaymentMethods({ token: tokenFromStore }); + if (isPaymentMethodExist.status === "error") { + return setIsPaymentModalOpen(true); + } + // if (!isPayedFirstPurchase) { + // return setIsPaymentModalOpen(true); + // } const { _id, key } = currentProduct; const paymentInfo = { productId: _id, @@ -197,7 +209,7 @@ function ExpertChat() { returnUrl, api ); - setPaymentIntent(paymentIntent); + // setPaymentIntent(paymentIntent); if ("payment" in paymentIntent) { if (paymentIntent.payment.status === "paid") return closeModals(); return setIsError(true); @@ -249,27 +261,41 @@ function ExpertChat() { ); }; + const onPaymentError = () => { + setIsPaymentModalOpen(false); + return setIsError(true); + } + + const onPaymentSuccess = () => { + setIsPaymentModalOpen(false); + setIsLoadingPayment(false); + return closeModals(); + } + return (
{!isLoading && - paymentIntent && - "paymentIntent" in paymentIntent && !!tokenFromStore.length && currentProduct && ( <> setPaymentIntent(null)} + open={isPaymentModalOpen} + onClose={() => setIsPaymentModalOpen(false)} containerClassName={styles.modal} > {getPriceCentsToDollars(currentProduct.price || 0)}$ - */} + diff --git a/src/components/EmailMarketing/v1/components/CustomerCounter/index.tsx b/src/components/EmailMarketing/v1/components/CustomerCounter/index.tsx index d390b9a..98ff065 100644 --- a/src/components/EmailMarketing/v1/components/CustomerCounter/index.tsx +++ b/src/components/EmailMarketing/v1/components/CustomerCounter/index.tsx @@ -1,3 +1,4 @@ +import { images } from '../../data'; import styles from './styles.module.scss'; interface CustomerCounterProps { @@ -7,7 +8,7 @@ interface CustomerCounterProps { function CustomerCounter({ count }: CustomerCounterProps) { return (
-
+ {/*
-
+
*/} +
{count}
); diff --git a/src/components/EmailMarketing/v1/components/CustomerCounter/styles.module.scss b/src/components/EmailMarketing/v1/components/CustomerCounter/styles.module.scss index d0a3bca..d18969f 100644 --- a/src/components/EmailMarketing/v1/components/CustomerCounter/styles.module.scss +++ b/src/components/EmailMarketing/v1/components/CustomerCounter/styles.module.scss @@ -2,7 +2,7 @@ position: relative; width: 263px; height: 263px; - background: #7A6BE2; + // background: #7A6BE2; border-radius: 50%; display: flex; align-items: center; @@ -16,8 +16,8 @@ .circularText { position: absolute; - width: 100%; - height: 100%; + width: calc(100% + 24px); + height: calc(100% + 24px); animation: rotate 20s linear infinite; -webkit-transform: translateZ(0); -webkit-perspective: 1000; diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index fcb8e6f..f61bcc5 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useEffect } from "react"; +import { ReactNode, useCallback, useEffect, useRef, useState } from "react"; import styles from "./styles.module.css"; interface ModalProps { @@ -8,7 +8,7 @@ interface ModalProps { className?: string; containerClassName?: string; type?: "hidden" | "normal"; - onClose?: () => void; + onClose: () => void; removeNoScroll?: boolean; } @@ -22,6 +22,8 @@ function Modal({ onClose, removeNoScroll = true }: ModalProps): JSX.Element { + const modalContentRef = useRef(null); + const handleClose = (event: React.MouseEvent) => { if (event.target !== event.currentTarget) return; document.body.classList.remove("no-scroll"); @@ -42,18 +44,53 @@ function Modal({ }; }, [open, removeNoScroll]); + const [position, setPosition] = useState({ top: 0, left: 0 }); + + const getModalContentPosition = useCallback(() => { + const modalContent = modalContentRef.current; + if (!modalContent) return { + top: 0, + left: 0 + }; + const { top, left } = modalContent.getBoundingClientRect(); + return { top, left }; + }, [modalContentRef]); + + useEffect(() => { + const updatePosition = () => { + requestAnimationFrame(() => { + setPosition(getModalContentPosition()); + }); + }; + + if (open) { + updatePosition(); + } + + window.addEventListener('resize', updatePosition); + + return () => { + window.removeEventListener('resize', updatePosition); + }; + }, [getModalContentPosition, open]); + if (!open && type === "normal") return <>; + return (
-
- {isCloseButtonVisible && ( -
diff --git a/src/components/Modal/styles.module.css b/src/components/Modal/styles.module.css index 24e5388..d822b00 100644 --- a/src/components/Modal/styles.module.css +++ b/src/components/Modal/styles.module.css @@ -53,6 +53,7 @@ background-position: center; background-color: transparent; background-image: url(./close.svg); + z-index: 4; } .modal .main-btn { diff --git a/src/components/PalmistryV1/components/Footer/styles.module.scss b/src/components/PalmistryV1/components/Footer/styles.module.scss index 0061d2d..ae37d88 100644 --- a/src/components/PalmistryV1/components/Footer/styles.module.scss +++ b/src/components/PalmistryV1/components/Footer/styles.module.scss @@ -1,6 +1,6 @@ .footer { width: 100%; - margin-top: 30px; + margin: 30px 0 62px; display: flex; flex-direction: column; align-items: center; diff --git a/src/components/PalmistryV1/components/PaymentModalV1/PaymentForm/index.tsx b/src/components/PalmistryV1/components/PaymentModalV1/PaymentForm/index.tsx index 31daf17..c1486f3 100644 --- a/src/components/PalmistryV1/components/PaymentModalV1/PaymentForm/index.tsx +++ b/src/components/PalmistryV1/components/PaymentModalV1/PaymentForm/index.tsx @@ -100,7 +100,7 @@ function PaymentForm({ )} > - + setIsPaymentModalOpen(false)}> diff --git a/src/components/PalmistryV1/pages/TrialPayment/index.tsx b/src/components/PalmistryV1/pages/TrialPayment/index.tsx index 7cfbd0b..fa430fc 100644 --- a/src/components/PalmistryV1/pages/TrialPayment/index.tsx +++ b/src/components/PalmistryV1/pages/TrialPayment/index.tsx @@ -14,9 +14,11 @@ import { useNavigate } from "react-router-dom"; import { useTranslations } from "@/hooks/translations"; import { ELocalesPlacement } from "@/locales"; import { usePreloadImages } from "@/hooks/preload/images"; +import useTimer from "@/hooks/palmistry/use-timer"; function TrialPayment() { const { translate } = useTranslations(ELocalesPlacement.PalmistryV1); + const time = useTimer(); const navigate = useNavigate(); usePreloadImages([ "/v1/palmistry/ticket.svg", @@ -66,6 +68,24 @@ function TrialPayment() { Partners