diff --git a/environments/.env.develop b/environments/.env.develop index 1aaa843..2e275bb 100644 --- a/environments/.env.develop +++ b/environments/.env.develop @@ -1,5 +1,6 @@ AURA_API_HOST=https://api-web.aura.wit.life AURA_DAPI_HOST=https://dev.api.aura.witapps.us +AURA_DAPI_PREFIX=v2 AURA_SITE_HOST=https://aura.wit.life AURA_PREFIX=api/v1 AURA_OPEN_AI_HOST=https://api.openai.com diff --git a/environments/.env.production b/environments/.env.production index 05cd7ea..178f647 100644 --- a/environments/.env.production +++ b/environments/.env.production @@ -1,5 +1,6 @@ AURA_API_HOST=https://api-web.aura.wit.life AURA_DAPI_HOST=https://api.aura.witapps.us +AURA_DAPI_PREFIX=v2 AURA_SITE_HOST=https://aura.wit.life AURA_PREFIX=api/v1 AURA_OPEN_AI_HOST=https://api.openai.com diff --git a/index.html b/index.html index bd8bd4c..05b8e9c 100755 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> - + @@ -69,7 +69,7 @@ - diff --git a/public/leo.webp b/public/leo.webp new file mode 100644 index 0000000..0cf37e8 Binary files /dev/null and b/public/leo.webp differ diff --git a/public/questionnaire-redesign/zodiacs/female/pdf.sex.AQUARIUS.FEMALE.webp b/public/questionnaire-redesign/zodiacs/female/pdf.sex.AQUARIUS.FEMALE.webp new file mode 100644 index 0000000..574cc7c Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/female/pdf.sex.AQUARIUS.FEMALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/female/pdf.sex.ARIES.FEMALE.webp b/public/questionnaire-redesign/zodiacs/female/pdf.sex.ARIES.FEMALE.webp new file mode 100644 index 0000000..fe1ab38 Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/female/pdf.sex.ARIES.FEMALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/female/pdf.sex.CANCER.FEMALE.webp b/public/questionnaire-redesign/zodiacs/female/pdf.sex.CANCER.FEMALE.webp new file mode 100644 index 0000000..c7415c6 Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/female/pdf.sex.CANCER.FEMALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/female/pdf.sex.CAPRICORN.FEMALE.webp b/public/questionnaire-redesign/zodiacs/female/pdf.sex.CAPRICORN.FEMALE.webp new file mode 100644 index 0000000..53bef7f Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/female/pdf.sex.CAPRICORN.FEMALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/female/pdf.sex.GEMINI.FEMALE.webp b/public/questionnaire-redesign/zodiacs/female/pdf.sex.GEMINI.FEMALE.webp new file mode 100644 index 0000000..372800f Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/female/pdf.sex.GEMINI.FEMALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/female/pdf.sex.LEO.FEMALE.webp b/public/questionnaire-redesign/zodiacs/female/pdf.sex.LEO.FEMALE.webp new file mode 100644 index 0000000..8ba00d8 Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/female/pdf.sex.LEO.FEMALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/female/pdf.sex.LIBRA.FEMALE.webp b/public/questionnaire-redesign/zodiacs/female/pdf.sex.LIBRA.FEMALE.webp new file mode 100644 index 0000000..7d40824 Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/female/pdf.sex.LIBRA.FEMALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/female/pdf.sex.PISCES.FEMALE.webp b/public/questionnaire-redesign/zodiacs/female/pdf.sex.PISCES.FEMALE.webp new file mode 100644 index 0000000..2bf3a5e Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/female/pdf.sex.PISCES.FEMALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/female/pdf.sex.SAGITTARIUS.FEMALE.webp b/public/questionnaire-redesign/zodiacs/female/pdf.sex.SAGITTARIUS.FEMALE.webp new file mode 100644 index 0000000..9cea2cc Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/female/pdf.sex.SAGITTARIUS.FEMALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/female/pdf.sex.SCORPIO.FEMALE.webp b/public/questionnaire-redesign/zodiacs/female/pdf.sex.SCORPIO.FEMALE.webp new file mode 100644 index 0000000..ca866f4 Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/female/pdf.sex.SCORPIO.FEMALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/female/pdf.sex.TAURUS.FEMALE.webp b/public/questionnaire-redesign/zodiacs/female/pdf.sex.TAURUS.FEMALE.webp new file mode 100644 index 0000000..7e258ea Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/female/pdf.sex.TAURUS.FEMALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/female/pdf.sex.VIRGO.FEMALE.webp b/public/questionnaire-redesign/zodiacs/female/pdf.sex.VIRGO.FEMALE.webp new file mode 100644 index 0000000..0978736 Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/female/pdf.sex.VIRGO.FEMALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/male/pdf.sex.AQUARIUS.MALE.webp b/public/questionnaire-redesign/zodiacs/male/pdf.sex.AQUARIUS.MALE.webp new file mode 100644 index 0000000..df118bc Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/male/pdf.sex.AQUARIUS.MALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/male/pdf.sex.ARIES.MALE.webp b/public/questionnaire-redesign/zodiacs/male/pdf.sex.ARIES.MALE.webp new file mode 100644 index 0000000..5754005 Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/male/pdf.sex.ARIES.MALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/male/pdf.sex.CANCER.MALE.webp b/public/questionnaire-redesign/zodiacs/male/pdf.sex.CANCER.MALE.webp new file mode 100644 index 0000000..48f11c7 Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/male/pdf.sex.CANCER.MALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/male/pdf.sex.CAPRICORN.MALE.webp b/public/questionnaire-redesign/zodiacs/male/pdf.sex.CAPRICORN.MALE.webp new file mode 100644 index 0000000..497412f Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/male/pdf.sex.CAPRICORN.MALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/male/pdf.sex.GEMINI.MALE.webp b/public/questionnaire-redesign/zodiacs/male/pdf.sex.GEMINI.MALE.webp new file mode 100644 index 0000000..4962aaa Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/male/pdf.sex.GEMINI.MALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/male/pdf.sex.LEO.MALE.webp b/public/questionnaire-redesign/zodiacs/male/pdf.sex.LEO.MALE.webp new file mode 100644 index 0000000..22aab53 Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/male/pdf.sex.LEO.MALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/male/pdf.sex.LIBRA.MALE.webp b/public/questionnaire-redesign/zodiacs/male/pdf.sex.LIBRA.MALE.webp new file mode 100644 index 0000000..7c6b842 Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/male/pdf.sex.LIBRA.MALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/male/pdf.sex.PISCES.MALE.webp b/public/questionnaire-redesign/zodiacs/male/pdf.sex.PISCES.MALE.webp new file mode 100644 index 0000000..590709a Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/male/pdf.sex.PISCES.MALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/male/pdf.sex.SAGITTARIUS.MALE.webp b/public/questionnaire-redesign/zodiacs/male/pdf.sex.SAGITTARIUS.MALE.webp new file mode 100644 index 0000000..422b5c8 Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/male/pdf.sex.SAGITTARIUS.MALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/male/pdf.sex.SCORPIO.MALE.webp b/public/questionnaire-redesign/zodiacs/male/pdf.sex.SCORPIO.MALE.webp new file mode 100644 index 0000000..32d8d5d Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/male/pdf.sex.SCORPIO.MALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/male/pdf.sex.TAURUS.MALE.webp b/public/questionnaire-redesign/zodiacs/male/pdf.sex.TAURUS.MALE.webp new file mode 100644 index 0000000..d3a76f8 Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/male/pdf.sex.TAURUS.MALE.webp differ diff --git a/public/questionnaire-redesign/zodiacs/male/pdf.sex.VIRGO.MALE.webp b/public/questionnaire-redesign/zodiacs/male/pdf.sex.VIRGO.MALE.webp new file mode 100644 index 0000000..f0d293b Binary files /dev/null and b/public/questionnaire-redesign/zodiacs/male/pdf.sex.VIRGO.MALE.webp differ diff --git a/src/api/api.ts b/src/api/api.ts index 963151a..4acb49b 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -11,7 +11,6 @@ import { DailyForecasts, SubscriptionItems, SubscriptionCheckout, - SubscriptionReceipts, SubscriptionStatus, AICompatCategories, AICompats, @@ -28,6 +27,8 @@ import { SinglePayment, Products, Palmistry, + Paywall, + Payment, } from './resources' const api = { @@ -48,8 +49,8 @@ const api = { getSubscriptionPlans: createMethod(SubscriptionPlans.createRequest), getSubscriptionCheckout: createMethod(SubscriptionCheckout.createRequest), getSubscriptionStatus: createMethod(SubscriptionStatus.createRequest), - getSubscriptionReceipt: createMethod(SubscriptionReceipts.createGetRequest), - createSubscriptionReceipt: createMethod(SubscriptionReceipts.createRequest), + // new get subscription status + getSubscriptionStatusNew: createMethod(SubscriptionStatus.createRequestNew), getAiCompatCategories: createMethod(AICompatCategories.createRequest), getAiCompat: createMethod(AICompats.createRequest), getAiRequest: createMethod(AIRequests.createRequest), @@ -75,6 +76,10 @@ const api = { getPalmistryLines: createMethod(Palmistry.createRequest), // New Authorization authorization: createMethod(User.createAuthorizeRequest), + // Paywall + getPaywallByPlacementKey: createMethod(Paywall.createRequestGet), + // Payment + makePayment: createMethod(Payment.createRequestPost), } export type ApiContextValue = typeof api diff --git a/src/api/resources/Payment.ts b/src/api/resources/Payment.ts new file mode 100644 index 0000000..0de0ef2 --- /dev/null +++ b/src/api/resources/Payment.ts @@ -0,0 +1,46 @@ +import routes from "@/routes"; +import { getAuthHeaders } from "../utils"; + +interface Payload { + token: string; +} + +export interface PayloadPost extends Payload { + productId: string; +} + +interface ResponsePostSuccess { + status: "payment_intent_created" | "paid" | unknown, + type: "setup" | "payment", + data: { + client_secret: string, + paymentIntentId: string, + return_url?: string, + public_key: string, + product: { + id: string, + name: string, + description?: string, + price: { + id: string, + unit_amount: number, + currency: "USD" | string + } + } + } +} + +interface ResponsePostError { + status: string; + message: string; +} + +export type ResponsePost = ResponsePostSuccess | ResponsePostError; + +export const createRequestPost = ({ token, productId }: PayloadPost): Request => { + const url = new URL(routes.server.makePayment()); + const body = JSON.stringify({ + productId + }); + return new Request(url, { method: "POST", headers: getAuthHeaders(token), body }); +}; diff --git a/src/api/resources/Paywall.ts b/src/api/resources/Paywall.ts new file mode 100644 index 0000000..7f5030e --- /dev/null +++ b/src/api/resources/Paywall.ts @@ -0,0 +1,67 @@ +import routes from "@/routes"; +import { getAuthHeaders } from "../utils"; + +interface Payload { + token: string; +} + +export interface PayloadGet extends Payload { + placementKey: EPlacementKeys; +} + +export enum EPlacementKeys { + "aura.placement.main" = "aura.placement.main", + "aura.placement.redesign.main" = "aura.placement.redesign.main", + "aura.placement.email.marketing" = "aura.placement.email.marketing", + "aura.placement.secret.discount" = "aura.placement.secret.discount", + "aura.placement.palmistry.main" = "aura.placement.palmistry.main" +} + +interface ResponseGetSuccess { + paywall: IPaywall; +} + +interface ResponseGetError { + status: string; + message: string; +} + +export interface IPaywall { + _id: string; + key: string; + name: string; + products: IPaywallProduct[]; + properties: IPaywallProperties[]; +} + +export interface IPaywallProduct { + _id: string; + key: string; + productId: string; + name: string; + priceId: string; + type: string; + description: string; + discountPrice: null; + discountPriceId: null; + isDiscount: boolean; + isFreeTrial: boolean; + isTrial: boolean; + price: number; + trialDuration: number; + trialPrice: number; + trialPriceId: string; +} + +interface IPaywallProperties { + _id: string; + key: string; + value: string; +} + +export type ResponseGet = ResponseGetSuccess | ResponseGetError; + +export const createRequestGet = ({ token, placementKey }: PayloadGet): Request => { + const url = new URL(routes.server.getPaywallByPlacementKey(placementKey)); + return new Request(url, { method: "GET", headers: getAuthHeaders(token) }); +}; diff --git a/src/api/resources/SinglePayment.ts b/src/api/resources/SinglePayment.ts index f99a6ff..a0cc4ea 100644 --- a/src/api/resources/SinglePayment.ts +++ b/src/api/resources/SinglePayment.ts @@ -32,10 +32,18 @@ export interface PayloadPost extends Payload { } export interface ResponseGet { - key: string; - productId: string; - amount: number; - currency: string; + _id: string, + key: string, + name: string, + type: string, + description: string, + discountPrice: null | unknown, + isDiscount: boolean, + isFreeTrial: boolean, + isTrial: boolean, + price: number, + trialDuration: number, + trialPrice: number } interface ResponsePostNewPaymentData { diff --git a/src/api/resources/UserSubscriptionStatus.ts b/src/api/resources/UserSubscriptionStatus.ts index 32c5a79..caca009 100644 --- a/src/api/resources/UserSubscriptionStatus.ts +++ b/src/api/resources/UserSubscriptionStatus.ts @@ -14,3 +14,12 @@ export const createRequest = ({ token }: Payload): Request => { const url = new URL(routes.server.subscriptionStatus()) return new Request(url, { method: 'GET', headers: getAuthHeaders(token) }) } + +export interface ResponseNew { + subscription: boolean; +} + +export const createRequestNew = ({ token }: Payload): Request => { + const url = new URL(routes.server.subscriptionStatusNew()) + return new Request(url, { method: 'GET', headers: getAuthHeaders(token) }) +} diff --git a/src/api/resources/index.ts b/src/api/resources/index.ts index 32faca6..4123777 100644 --- a/src/api/resources/index.ts +++ b/src/api/resources/index.ts @@ -10,7 +10,6 @@ export * as AuthTokens from "./AuthTokens"; export * as SubscriptionItems from "./UserSubscriptionItemPrices"; export * as SubscriptionCheckout from "./UserSubscriptionCheckout"; export * as SubscriptionStatus from "./UserSubscriptionStatus"; -export * as SubscriptionReceipts from "./UserSubscriptionReceipts"; export * as AICompatCategories from "./AICompatCategories"; export * as AICompats from "./AICompats"; export * as AIRequests from "./AIRequests"; @@ -26,3 +25,5 @@ export * as OpenAI from "./OpenAI"; export * as SinglePayment from "./SinglePayment"; export * as Products from "./Products"; export * as Palmistry from "./Palmistry"; +export * as Paywall from "./Paywall"; +export * as Payment from "./Payment"; diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 29eeeae..94b8f75 100755 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -57,7 +57,6 @@ import { Asset } from "@/api/resources/Assets"; import PaymentResultPage from "../PaymentPage/results"; import PaymentSuccessPage from "../PaymentPage/results/SuccessPage"; import PaymentFailPage from "../PaymentPage/results/ErrorPage"; -import { StripePage } from "../StripePage"; import AuthPage from "../AuthPage"; import AuthResultPage from "../AuthResultPage"; import MagicBallPage from "../pages/MagicBall"; @@ -113,8 +112,8 @@ 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 SuccessPaymentPage from "../pages/PaymentWithEmailPage/ResultPayment/SuccessPaymentPage"; -import FailPaymentPage from "../pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage"; +import SuccessPaymentPage from "../pages/SinglePaymentPage/ResultPayment/SuccessPaymentPage"; +import FailPaymentPage from "../pages/SinglePaymentPage/ResultPayment/FailPaymentPage"; import { useSchemeColorByElement } from "@/hooks/useSchemeColorByElement"; import GetInformationPartnerPage from "../pages/GetInformationPartner"; import BirthPlacePage from "../pages/BirthPlacePage"; @@ -162,6 +161,10 @@ function App(): JSX.Element { const birthdate = user?.profile?.birthday || birthdateFromStore; const birthPlace = user?.profile?.birthplace || birthPlaceFromStore; + useLayoutEffect(() => { + dispatch(actions.paywalls.resetIsMustUpdate()); + }, [dispatch]); + useEffect(() => { // api.getAppConfig({ bundleId: "auraweb" }), dispatch( @@ -219,10 +222,13 @@ function App(): JSX.Element { token, }); - if (has_subscription && user) { + const { subscription: subscriptionStatusNew } = + await api.getSubscriptionStatusNew({ token }); + + if ((has_subscription || subscriptionStatusNew) && user) { return dispatch(actions.status.update("subscribed")); } - if (!has_subscription && user) { + if (!has_subscription && !subscriptionStatusNew && user) { return dispatch(actions.status.update("unsubscribed")); } if (!user) { @@ -292,7 +298,9 @@ function App(): JSX.Element { } - /> + > + } /> + } - /> + > + } /> + {/* Advisor short path */} - {/* Single Payment Page Short Path */} - {/* - } - > - } - > - } /> - - */} - {/* Single Payment Page Short Path */} - {/* Test Routes Start */} } /> }> @@ -805,10 +787,6 @@ function App(): JSX.Element { /> } /> } /> - {/* } - /> */} }> }> }> - {/* } - /> */} } /> - } - /> }> } /> diff --git a/src/components/EmailEnterPage/index.tsx b/src/components/EmailEnterPage/index.tsx index d2bf28b..d3377c6 100755 --- a/src/components/EmailEnterPage/index.tsx +++ b/src/components/EmailEnterPage/index.tsx @@ -4,7 +4,6 @@ import { useNavigate, useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { actions, selectors } from "@/store"; -import { useApi } from "@/api"; import Title from "../Title"; import Policy from "../Policy"; import EmailInput from "./EmailInput"; @@ -12,9 +11,10 @@ import MainButton from "../MainButton"; import Loader, { LoaderColor } from "../Loader"; 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"; +import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall"; +import { usePaywall } from "@/hooks/paywall/usePaywall"; interface IEmailEnterPage { redirectUrl?: string; @@ -25,8 +25,7 @@ function EmailEnterPage({ redirectUrl = routes.client.emailConfirm(), isRequiredName = false, }: IEmailEnterPage): JSX.Element { - const api = useApi(); - const { t, i18n } = useTranslation(); + const { t } = useTranslation(); const dispatch = useDispatch(); const navigate = useNavigate(); const [email, setEmail] = useState(""); @@ -35,51 +34,31 @@ function EmailEnterPage({ const [isValidEmail, setIsValidEmail] = useState(false); const [isValidName, setIsValidName] = useState(!isRequiredName); const [isAuth, setIsAuth] = useState(false); - const [subPlans, setSubPlans] = useState([]); - const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan); - const [activeSubPlan, setActiveSubPlan] = useState( - activeSubPlanFromStore - ); - const locale = i18n.language; + const activeProductFromStore = useSelector(selectors.selectActiveProduct); const { subPlan } = useParams(); const { error, isLoading, authorization } = useAuthentication(); + const { products } = usePaywall({ + placementKey: EPlacementKeys["aura.placement.main"], + }); + const [activeProduct, setActiveProduct] = useState( + activeProductFromStore + ); useEffect(() => { if (subPlan) { - const targetSubPlan = subPlans.find( - (sub_plan) => + const targetProduct = products.find( + (product) => String( - sub_plan?.trial?.price_cents - ? Math.floor((sub_plan?.trial?.price_cents + 1) / 100) - : sub_plan.id.replace(".", "") + product?.trialPrice + ? Math.floor((product?.trialPrice + 1) / 100) + : product.key.replace(".", "") ) === subPlan ); - if (targetSubPlan) { - setActiveSubPlan(targetSubPlan); + if (targetProduct) { + setActiveProduct(targetProduct); } } - }, [subPlan, subPlans]); - - useEffect(() => { - (async () => { - const { sub_plans } = await api.getSubscriptionPlans({ locale }); - const plans = sub_plans - .filter((plan: ISubscriptionPlan) => plan.provider === "stripe") - .sort((a, b) => { - if (!a.trial || !b.trial) { - return 0; - } - if (a?.trial?.price_cents < b?.trial?.price_cents) { - return -1; - } - if (a?.trial?.price_cents > b?.trial?.price_cents) { - return 1; - } - return 0; - }); - setSubPlans(plans); - })(); - }, [api, locale]); + }, [subPlan, products]); const handleValidEmail = (email: string) => { dispatch(actions.form.addEmail(email)); @@ -124,7 +103,7 @@ function EmailEnterPage({ await authorization(email, source); dispatch( actions.payment.update({ - activeSubPlan, + activeProduct, }) ); setIsAuth(true); diff --git a/src/components/EmailsList/index.tsx b/src/components/EmailsList/index.tsx index 12ee577..08d1f49 100755 --- a/src/components/EmailsList/index.tsx +++ b/src/components/EmailsList/index.tsx @@ -1,7 +1,6 @@ import { getRandomArbitrary, getRandomName } from "@/services/random-value"; import EmailItem, { IEmailItemProps } from "../EmailItem"; import styles from "./styles.module.css"; -import { useTranslation } from "react-i18next"; import { useEffect, useRef, useState } from "react"; const getEmails = (): IEmailItemProps[] => { @@ -18,6 +17,7 @@ const getEmails = (): IEmailItemProps[] => { }; interface IEmailsListProps { + title: string | JSX.Element | JSX.Element[]; classNameContainer?: string; classNameTitle?: string; classNameEmailItem?: string; @@ -25,25 +25,16 @@ interface IEmailsListProps { } function EmailsList({ + title, classNameContainer = "", classNameTitle = "", classNameEmailItem = "", direction = "up-down", }: IEmailsListProps): JSX.Element { - const { t } = useTranslation(); - const [countUsers, setCountUsers] = useState(752); const [emails, setEmails] = useState(getEmails()); const [elementIdx, setElementIdx] = useState(0); const itemsRef = useRef([]); - useEffect(() => { - const randomDelay = getRandomArbitrary(3000, 5000); - const countUsersTimeOut = setTimeout(() => { - setCountUsers((prevState) => prevState + 1); - }, randomDelay); - return () => clearTimeout(countUsersTimeOut); - }, [countUsers]); - useEffect(() => { let randomDelay = getRandomArbitrary(500, 5000); if (!elementIdx) { @@ -69,11 +60,7 @@ function EmailsList({ return (
- - {t("people_joined_today", { - countPeoples: {countUsers}, - })} - + {title}
{emails.map(({ email, price }, idx) => (
{ - if (subPlan.trial) { - return subPlan.trial.price_cents; + const getAmountFromProduct = (subPlan: IPaywallProduct) => { + if (subPlan.isTrial) { + return subPlan.trialPrice; } - return subPlan.price_cents; + return subPlan.price; }; useEffect(() => { - if (!stripe || !elements || !activeSubPlan) { + if (!stripe || !elements || !activeProduct) { return; } @@ -50,8 +50,8 @@ function ApplePayButton({ country: "US", currency: "usd", total: { - label: activeSubPlan.name || "Subscription", - amount: getAmountFromSubPlan(activeSubPlan), + label: activeProduct.name || "Subscription", + amount: getAmountFromProduct(activeProduct), }, requestPayerName: true, requestPayerEmail: true, @@ -95,7 +95,6 @@ function ApplePayButton({ }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ - activeSubPlan, client_secret, dispatch, elements, diff --git a/src/components/StripePage/ApplePayButton/styles.module.css b/src/components/PaymentPage/methods/ApplePayButton/styles.module.css similarity index 100% rename from src/components/StripePage/ApplePayButton/styles.module.css rename to src/components/PaymentPage/methods/ApplePayButton/styles.module.css diff --git a/src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx b/src/components/PaymentPage/methods/CheckoutForm/index.tsx similarity index 93% rename from src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx rename to src/components/PaymentPage/methods/CheckoutForm/index.tsx index 0cc4cd5..308cf27 100644 --- a/src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx +++ b/src/components/PaymentPage/methods/CheckoutForm/index.tsx @@ -16,12 +16,14 @@ interface ICheckoutFormProps { children?: JSX.Element | null; subscriptionReceiptId?: string; returnUrl?: string; + confirmType?: "payment" | "setup"; } export default function CheckoutForm({ children, subscriptionReceiptId, returnUrl, + confirmType = "payment", }: ICheckoutFormProps) { const stripe = useStripe(); const elements = useElements(); @@ -42,7 +44,9 @@ export default function CheckoutForm({ setIsProcessing(true); try { - const { error } = await stripe.confirmPayment({ + const { error } = await stripe[ + confirmType === "payment" ? "confirmPayment" : "confirmSetup" + ]({ elements, confirmParams: { return_url: returnUrl diff --git a/src/components/PaymentPage/methods/Stripe/styles.module.css b/src/components/PaymentPage/methods/CheckoutForm/styles.module.css similarity index 100% rename from src/components/PaymentPage/methods/Stripe/styles.module.css rename to src/components/PaymentPage/methods/CheckoutForm/styles.module.css diff --git a/src/components/PaymentPage/methods/Stripe/Button.tsx b/src/components/PaymentPage/methods/Stripe/Button.tsx deleted file mode 100644 index cba41d3..0000000 --- a/src/components/PaymentPage/methods/Stripe/Button.tsx +++ /dev/null @@ -1,16 +0,0 @@ - -import { useTranslation } from 'react-i18next' -import MainButton from '@/components/MainButton' - -interface IStripeButtonProps { - onClick: () => void -} - -export function StripeButton({ onClick }: IStripeButtonProps): JSX.Element { - const { t } = useTranslation() - return ( - - {t('stripe')} - - ) -} diff --git a/src/components/PaymentPage/methods/Stripe/Modal.tsx b/src/components/PaymentPage/methods/Stripe/Modal.tsx deleted file mode 100644 index cea4896..0000000 --- a/src/components/PaymentPage/methods/Stripe/Modal.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import styles from "./styles.module.css"; -import { useApi } from "@/api"; -import Modal from "@/components/Modal"; -import Loader from "@/components/Loader"; -import { useEffect, useState } from "react"; -import { Stripe, loadStripe } from "@stripe/stripe-js"; -import { Elements } from "@stripe/react-stripe-js"; -import CheckoutForm from "./CheckoutForm"; -import { useAuth } from "@/auth"; -import { useSelector } from "react-redux"; -import { selectors } from "@/store"; -import Title from "@/components/Title"; -import ApplePayButton from "@/components/StripePage/ApplePayButton"; -import SubPlanInformation from "@/components/SubPlanInformation"; -import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; -import routes from "@/routes"; - -interface StripeModalProps { - open: boolean; - onClose: () => void; - // onSuccess: (receipt: SubscriptionReceipts.SubscriptionReceipt) => void; - // onError: (error: Error) => void; -} - -export function StripeModal({ - open, - onClose, -}: // onSuccess, -// onError, -StripeModalProps): JSX.Element { - const { i18n } = useTranslation(); - const api = useApi(); - const { token } = useAuth(); - const locale = i18n.language; - const navigate = useNavigate(); - const activeSubPlan = useSelector(selectors.selectActiveSubPlan); - const email = useSelector(selectors.selectUser).email; - const [stripePromise, setStripePromise] = - useState | null>(null); - const [clientSecret, setClientSecret] = useState(""); - const [subscriptionReceiptId, setSubscriptionReceiptId] = - useState(""); - const [isLoading, setIsLoading] = useState(true); - if (!activeSubPlan) { - navigate(routes.client.trialChoice()); - } - - useEffect(() => { - (async () => { - const siteConfig = await api.getAppConfig({ bundleId: "auraweb" }); - setStripePromise(loadStripe(siteConfig.data.stripe_public_key)); - const { sub_plans } = await api.getSubscriptionPlans({ locale }); - const isActiveSubPlan = sub_plans.find( - (subPlan) => subPlan.id === activeSubPlan?.id - ); - if (!activeSubPlan || !isActiveSubPlan) { - navigate(routes.client.priceList()); - } - })(); - }, [activeSubPlan, api, locale, navigate]); - - useEffect(() => { - (async () => { - const { subscription_receipt } = await api.createSubscriptionReceipt({ - token, - way: "stripe", - subscription_receipt: { - sub_plan_id: activeSubPlan?.id || "stripe.7", - }, - }); - const { id } = subscription_receipt; - const { client_secret } = subscription_receipt.data; - setSubscriptionReceiptId(id); - setClientSecret(client_secret); - setIsLoading(false); - })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [api, token]); - - const handleClose = () => { - onClose(); - }; - - return ( - - {isLoading ? ( -
- -
- ) : null} - {!isLoading && ( - <> - - Choose payment method - -

{email}

- - )} - {stripePromise && clientSecret && subscriptionReceiptId && ( - - - {activeSubPlan && ( - - )} - - - )} -
- ); -} diff --git a/src/components/PaymentPage/methods/Stripe/index.tsx b/src/components/PaymentPage/methods/Stripe/index.tsx deleted file mode 100644 index 794d8c4..0000000 --- a/src/components/PaymentPage/methods/Stripe/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export * from './Button' -export * from './Modal' diff --git a/src/components/PaymentPage/results/index.tsx b/src/components/PaymentPage/results/index.tsx index 91bca33..8a2a4ea 100644 --- a/src/components/PaymentPage/results/index.tsx +++ b/src/components/PaymentPage/results/index.tsx @@ -3,89 +3,17 @@ import { useEffect, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { useDispatch } from "react-redux"; import { actions } from "@/store"; -// import { SubscriptionReceipts, useApi, useApiCall } from "@/api"; -// import { useAuth } from "@/auth"; import styles from "./styles.module.css"; import Loader from "@/components/Loader"; import { paymentResultPathsOfProducts } from "@/data/products"; function PaymentResultPage(): JSX.Element { - // const api = useApi(); - // const { token } = useAuth(); const navigate = useNavigate(); const dispatch = useDispatch(); const [searchParams] = useSearchParams(); const status = searchParams.get("redirect_status"); const redirect_type = searchParams.get("redirect_type"); - // const { id } = useParams(); - // const requestTimeOutRef = useRef(); const [isLoading] = useState(true); - // const [subscriptionReceipt, setSubscriptionReceipt] = - // useState(); - - // const loadData = useCallback(async () => { - // if (!id) { - // return null; - // } - // const getSubscriptionReceiptStatus = async () => { - // const { subscription_receipt } = await api.getSubscriptionReceipt({ - // token, - // id, - // }); - // const { stripe_status } = subscription_receipt.data; - // if (stripe_status === "incomplete") { - // requestTimeOutRef.current = setTimeout( - // getSubscriptionReceiptStatus, - // 3000 - // ); - // } - // setSubscriptionReceipt(subscription_receipt); - // return { subscription_receipt }; - // }; - // return getSubscriptionReceiptStatus(); - // }, [api, id, token]); - - // useApiCall(loadData); - - // useEffect(() => { - // if (!subscriptionReceipt) { - // if (id?.length) return; - // return () => { - // if (requestTimeOutRef.current) { - // clearTimeout(requestTimeOutRef.current); - // } - // navigate(routes.client.paymentFail()); - // }; - // } - // const { stripe_status } = subscriptionReceipt.data; - // if (stripe_status === "succeeded") { - // dispatch(actions.status.update("subscribed")); - // setIsLoading(false); - // return () => { - // if (requestTimeOutRef.current) { - // clearTimeout(requestTimeOutRef.current); - // } - // navigate(routes.client.paymentSuccess()); - // }; - // } else if (stripe_status === "payment_failed") { - // setIsLoading(false); - - // return () => { - // if (requestTimeOutRef.current) { - // clearTimeout(requestTimeOutRef.current); - // } - // navigate(routes.client.paymentFail()); - // }; - // } - // }, [dispatch, id, navigate, subscriptionReceipt]); - - // useEffect(() => { - // return () => { - // if (requestTimeOutRef.current) { - // clearTimeout(requestTimeOutRef.current); - // } - // }; - // }, []); useEffect(() => { if (status === "succeeded") { diff --git a/src/components/PriceList/index.tsx b/src/components/PriceList/index.tsx index b086f0a..5b67986 100755 --- a/src/components/PriceList/index.tsx +++ b/src/components/PriceList/index.tsx @@ -3,37 +3,39 @@ import PriceItem from "../PriceItem"; import styles from "./styles.module.css"; import { useDispatch } from "react-redux"; import { actions } from "@/store"; -import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans"; +import { IPaywallProduct } from "@/api/resources/Paywall"; interface PriceListProps { - subPlans: ISubscriptionPlan[]; + products: IPaywallProduct[]; activeItem: number | null; classNameItem?: string; classNameItemActive?: string; click: () => void; } -const getPrice = (plan: ISubscriptionPlan) => { - return (plan.trial?.price_cents || 0) / 100; +const getPrice = (product: IPaywallProduct) => { + return (product.trialPrice || 0) / 100; }; function PriceList({ click, - subPlans, + products, classNameItem = "", classNameItemActive = "", }: PriceListProps): JSX.Element { const dispatch = useDispatch(); - const [activePlanItem, setActivePlanItem] = - useState(null); + const [activeProduct, setActiveProduct] = useState( + null + ); const priceItemClick = (id: string) => { - const activePlan = subPlans.find((item) => item.id === String(id)) || null; - setActivePlanItem(activePlan); - if (activePlan) { + const activeProduct = + products.find((item) => item._id === String(id)) || null; + setActiveProduct(activeProduct); + if (activeProduct) { dispatch( actions.payment.update({ - activeSubPlan: activePlan, + activeProduct, }) ); } @@ -42,12 +44,12 @@ function PriceList({ return (
- {subPlans.map((plan, idx) => ( + {products.map((product, idx) => ( ([]); const email = useSelector(selectors.selectEmail); + const { products, getText } = usePaywall({ + placementKey: EPlacementKeys["aura.placement.main"], + }); + const [countUsers, setCountUsers] = useState(752); useEffect(() => { - (async () => { - const { sub_plans } = await api.getSubscriptionPlans({ locale }); - const plans = sub_plans - .filter((plan: ISubscriptionPlan) => plan.provider === "stripe") - .sort((a, b) => { - if (!a.trial || !b.trial) { - return 0; - } - if (a.trial?.price_cents < b.trial?.price_cents) { - return -1; - } - if (a.trial?.price_cents > b.trial?.price_cents) { - return 1; - } - return 0; - }); - setSubPlans(plans); - })(); - }, [api, locale]); + const randomDelay = getRandomArbitrary(3000, 5000); + const countUsersTimeOut = setTimeout(() => { + setCountUsers((prevState) => prevState + 1); + }, randomDelay); + return () => clearTimeout(countUsersTimeOut); + }, [countUsers]); const handleNext = () => { dispatch( @@ -60,25 +49,33 @@ function PriceListPage(): JSX.Element { <>
- {!!subPlans.length && ( + {!!products.length && ( <> {t("choose_your_own_fee")}

{t("aura.web.price_selection")}

- +
)} - {!subPlans.length && } + {!products.length && }
); diff --git a/src/components/StripePage/index.tsx b/src/components/StripePage/index.tsx deleted file mode 100644 index 5b6336e..0000000 --- a/src/components/StripePage/index.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { useApi } from "@/api"; -import Loader from "@/components/Loader"; -import { useEffect, useState } from "react"; -import { Stripe, loadStripe } from "@stripe/stripe-js"; -import { Elements } from "@stripe/react-stripe-js"; -import CheckoutForm from "../PaymentPage/methods/Stripe/CheckoutForm"; -import { useAuth } from "@/auth"; -import styles from "./styles.module.css"; -import { useSelector } from "react-redux"; -import { selectors } from "@/store"; -import { useNavigate } from "react-router-dom"; -import routes from "@/routes"; -import SubPlanInformation from "../SubPlanInformation"; -import Title from "../Title"; -import { useTranslation } from "react-i18next"; -import ApplePayButton from "./ApplePayButton"; - -export function StripePage(): JSX.Element { - const { i18n } = useTranslation(); - const api = useApi(); - const { token } = useAuth(); - const locale = i18n.language; - const navigate = useNavigate(); - const activeSubPlan = useSelector(selectors.selectActiveSubPlan); - const email = useSelector(selectors.selectUser).email; - const [stripePromise, setStripePromise] = - useState | null>(null); - const [clientSecret, setClientSecret] = useState(""); - const [subscriptionReceiptId, setSubscriptionReceiptId] = - useState(""); - const [isLoading, setIsLoading] = useState(true); - if (!activeSubPlan) { - navigate(routes.client.priceList()); - } - - useEffect(() => { - (async () => { - const siteConfig = await api.getAppConfig({ bundleId: "auraweb" }); - setStripePromise(loadStripe(siteConfig.data.stripe_public_key)); - const { sub_plans } = await api.getSubscriptionPlans({ locale }); - const isActiveSubPlan = sub_plans.find( - (subPlan) => subPlan.id === activeSubPlan?.id - ); - if (!activeSubPlan || !isActiveSubPlan) { - navigate(routes.client.priceList()); - } - })(); - }, [activeSubPlan, api, locale, navigate]); - - useEffect(() => { - (async () => { - const { subscription_receipt } = await api.createSubscriptionReceipt({ - token, - way: "stripe", - subscription_receipt: { - sub_plan_id: activeSubPlan?.id || "stripe.7", - }, - }); - const { id } = subscription_receipt; - const { client_secret } = subscription_receipt.data; - setSubscriptionReceiptId(id); - setClientSecret(client_secret); - setIsLoading(false); - })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [api, token]); - - return ( -
- {isLoading ? ( -
- -
- ) : null} - {!isLoading && ( - <> - - Pay - -

{email}

- - )} - {stripePromise && clientSecret && subscriptionReceiptId && ( - - - {activeSubPlan && ( - - )} - - - )} -
- ); -} diff --git a/src/components/StripePage/styles.module.css b/src/components/StripePage/styles.module.css deleted file mode 100644 index d305530..0000000 --- a/src/components/StripePage/styles.module.css +++ /dev/null @@ -1,38 +0,0 @@ -.page { - /* position: relative; */ - position: static; - /* height: calc(100vh - 50px); - max-height: -webkit-fill-available; */ - display: flex; - justify-items: center; - justify-content: center; - gap: 16px; -} - -.payment-loader { - display: flex; - justify-content: center; - align-items: center; -} - -.cross { - position: absolute; - top: -36px; - right: 28px; - width: 22px; - height: 22px; - cursor: pointer; - z-index: 9; -} - -.title { - font-size: 27px; - font-weight: 700; - margin: 0; -} - -.email { - font-size: 17px; - font-weight: 500; - margin: 0; -} \ No newline at end of file diff --git a/src/components/SubPlanInformation/index.tsx b/src/components/SubPlanInformation/index.tsx index 5d991fa..c872f5c 100644 --- a/src/components/SubPlanInformation/index.tsx +++ b/src/components/SubPlanInformation/index.tsx @@ -1,34 +1,34 @@ import { useTranslation } from "react-i18next"; import styles from "./styles.module.css"; -import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans"; import TotalToday from "./TotalToday"; -import ApplePayButton from "../StripePage/ApplePayButton"; +import ApplePayButton from "../PaymentPage/methods/ApplePayButton"; +import { IPaywallProduct } from "@/api/resources/Paywall"; interface ISubPlanInformationProps { - subPlan: ISubscriptionPlan; + product: IPaywallProduct; client_secret?: string; } -const getPrice = (plan: ISubscriptionPlan): string => { +const getPrice = (product: IPaywallProduct): string => { return `$${ - (plan.trial?.price_cents === 100 ? 99 : plan.trial?.price_cents || 0) / 100 + (product.trialPrice === 100 ? 99 : product.trialPrice || 0) / 100 }`; }; function SubPlanInformation({ - subPlan, + product, client_secret, }: ISubPlanInformationProps): JSX.Element { const { t } = useTranslation(); return (
- + {client_secret && ( - + )}

- {t("auweb.pay.information").replaceAll("%@", getPrice(subPlan))}. + {t("auweb.pay.information").replaceAll("%@", getPrice(product))}.

); diff --git a/src/components/SubscriptionPage/index.tsx b/src/components/SubscriptionPage/index.tsx index 6ee9803..3a9680c 100644 --- a/src/components/SubscriptionPage/index.tsx +++ b/src/components/SubscriptionPage/index.tsx @@ -11,22 +11,23 @@ import styles from "./styles.module.css"; // import Header from "../Header"; // import SpecialWelcomeOffer from "../SpecialWelcomeOffer"; import { useEffect, useState } from "react"; -import { ISubscriptionPlan, ITrial } from "@/api/resources/SubscriptionPlans"; import { ApiError, extractErrorMessage, useApi } from "@/api"; import { useAuth } from "@/auth"; import { getClientLocale, getClientTimezone } from "@/locales"; import Loader from "../Loader"; import Title from "../Title"; import ErrorText from "../ErrorText"; +import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall"; +import { usePaywall } from "@/hooks/paywall/usePaywall"; const currency = Currency.USD; const locale = getClientLocale() as Locale; -const getPriceFromTrial = (trial: ITrial | null) => { - if (!trial) { +const getPrice = (product: IPaywallProduct | null) => { + if (!product?.trialPrice) { return 0; } - return (trial.price_cents === 100 ? 99 : trial.price_cents || 0) / 100; + return (product.trialPrice === 100 ? 99 : product.trialPrice || 0) / 100; }; function SubscriptionPage(): JSX.Element { @@ -48,37 +49,40 @@ function SubscriptionPage(): JSX.Element { const [isLoading, setIsLoading] = useState(false); const [apiError, setApiError] = useState(null); const [error, setError] = useState(false); - const [subPlans, setSubPlans] = useState([]); - const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan); - const [activeSubPlan, setActiveSubPlan] = useState( - activeSubPlanFromStore - ); const { subPlan } = useParams(); const birthday = useSelector(selectors.selectBirthday); - console.log(nameError) + console.log(nameError); + + const { products } = usePaywall({ + placementKey: EPlacementKeys["aura.placement.main"], + }); + const activeProductFromStore = useSelector(selectors.selectActiveProduct); + const [activeProduct, setActiveProduct] = useState( + activeProductFromStore + ); useEffect(() => { if (subPlan) { - const targetSubPlan = subPlans.find( - (sub_plan) => + const targetProduct = products.find( + (product) => String( - sub_plan?.trial?.price_cents - ? Math.floor((sub_plan?.trial?.price_cents + 1) / 100) - : sub_plan.id.replace(".", "") + product?.trialPrice + ? Math.floor((product?.trialPrice + 1) / 100) + : product.key.replace(".", "") ) === subPlan ); - if (targetSubPlan) { - setActiveSubPlan(targetSubPlan); + if (targetProduct) { + setActiveProduct(targetProduct); } } - }, [subPlan, subPlans]); + }, [products, subPlan]); const paymentItems = [ { - title: activeSubPlan?.name || "Per 7-Day Trial For", - price: getPriceFromTrial(activeSubPlan?.trial || null), - description: activeSubPlan?.desc.length - ? activeSubPlan?.desc + title: activeProduct?.name || "Per 7-Day Trial For", + price: getPrice(activeProduct), + description: activeProduct?.description?.length + ? activeProduct.description : t("au.2week_plan.web"), }, ]; @@ -111,7 +115,7 @@ function SubscriptionPage(): JSX.Element { dispatch(actions.status.update("registred")); dispatch( actions.payment.update({ - activeSubPlan, + activeProduct, }) ); setIsLoading(false); @@ -176,29 +180,6 @@ function SubscriptionPage(): JSX.Element { setName(name); }; - useEffect(() => { - (async () => { - const { sub_plans } = await api.getSubscriptionPlans({ locale }); - const plans = sub_plans - .filter( - (plan: ISubscriptionPlan) => plan.provider === "stripe" - ) - .sort((a, b) => { - if (!a.trial || !b.trial) { - return 0; - } - if (a?.trial?.price_cents < b?.trial?.price_cents) { - return -1; - } - if (a?.trial?.price_cents > b?.trial?.price_cents) { - return 1; - } - return 0; - }); - setSubPlans(plans); - })(); - }, [api]); - return ( <> {/* */} @@ -274,7 +255,7 @@ function SubscriptionPage(): JSX.Element {
- Start ${getPriceFromTrial(activeSubPlan?.trial || null)} + Start ${getPrice(activeProduct || null)}
diff --git a/src/components/pages/ABDesign/v1/components/PriceList/index.tsx b/src/components/pages/ABDesign/v1/components/PriceList/index.tsx index 649f673..54f77d2 100644 --- a/src/components/pages/ABDesign/v1/components/PriceList/index.tsx +++ b/src/components/pages/ABDesign/v1/components/PriceList/index.tsx @@ -2,38 +2,37 @@ import { useState } from "react"; import styles from "./styles.module.css"; import { useDispatch } from "react-redux"; import { actions } from "@/store"; -import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans"; import PriceItem from "../PriceItem"; +import { IPaywallProduct } from "@/api/resources/Paywall"; interface PriceListProps { - subPlans: ISubscriptionPlan[]; + products: IPaywallProduct[]; activeItem: number | null; classNameItem?: string; classNameItemActive?: string; click: () => void; } -const getPrice = (plan: ISubscriptionPlan) => { - return (plan.trial?.price_cents || 0) / 100; +const getPrice = (product: IPaywallProduct) => { + return (product.trialPrice || 0) / 100; }; function PriceList({ click, - subPlans, + products, classNameItem = "", classNameItemActive = "", }: PriceListProps): JSX.Element { const dispatch = useDispatch(); - const [activePlanItem, setActivePlanItem] = - useState(null); + const [activeProductItem, setActiveProductItem] = useState(); const priceItemClick = (id: string) => { - const activePlan = subPlans.find((item) => item.id === String(id)) || null; - setActivePlanItem(activePlan); - if (activePlan) { + const activeProduct = products.find((item) => item._id === String(id)); + setActiveProductItem(activeProduct); + if (activeProduct) { dispatch( actions.payment.update({ - activeSubPlan: activePlan, + activeProduct, }) ); } @@ -42,12 +41,12 @@ function PriceList({ return (
- {subPlans.map((plan, idx) => ( + {products.map((product, idx) => ( ( - currentlyAffecting?.split("$") || [] - ); const handleClick = (answer: IAnswer) => { - if (selectedAnswers.includes(answer.id)) { - return setSelectedAnswers((prevState) => - prevState.filter((item) => item !== answer.id) + if (currentlyAffecting.includes(`$${answer.id}`)) { + return dispatch( + actions.questionnaire.update({ + currentlyAffecting: currentlyAffecting.replace(`$${answer.id}`, ""), + }) ); } if (answer.id === "none_of_these") { - return setSelectedAnswers([answer.id]); + return dispatch( + actions.questionnaire.update({ + currentlyAffecting: `$${answer.id}`, + }) + ); } if ( - selectedAnswers.includes("none_of_these") && + currentlyAffecting.includes("$none_of_these") && answer.id !== "none_of_these" ) { return; } - return setSelectedAnswers((prevState) => [...prevState, answer.id]); + return dispatch( + actions.questionnaire.update({ + currentlyAffecting: `${currentlyAffecting}$${answer.id}`, + }) + ); }; const handleNext = () => { - dispatch( - actions.questionnaire.update({ - currentlyAffecting: selectedAnswers.join("$"), - }) - ); navigate( `${routes.client.questionnaireV1()}/relationships/partnerPriority` ); @@ -55,30 +56,35 @@ function MultiplyAnswers({ answers }: IMultiplyAnswersProps) { return ( <> - {answers.map((answer, index) => ( - handleClick(answer)} - /> - ))} - - {t("next")} - +
+ {answers.map((answer, index) => ( + handleClick(answer)} + /> + ))} +
+ {!!currentlyAffecting.length && ( + + {t("next")} + + )} ); } diff --git a/src/components/pages/ABDesign/v1/components/Questionnaire/CustomAnswers/MultipleAnswers/styles.module.css b/src/components/pages/ABDesign/v1/components/Questionnaire/CustomAnswers/MultipleAnswers/styles.module.css index fc7a13a..e8bd9e5 100644 --- a/src/components/pages/ABDesign/v1/components/Questionnaire/CustomAnswers/MultipleAnswers/styles.module.css +++ b/src/components/pages/ABDesign/v1/components/Questionnaire/CustomAnswers/MultipleAnswers/styles.module.css @@ -1,4 +1,19 @@ +.multiply-answers { + display: flex; + flex-direction: column; + align-items: center; + gap: 11px; + z-index: 0; + padding-bottom: 116px; + width: 100%; +} + + .button { + position: fixed; + bottom: calc(0dvh + 16px); + width: calc(100% - 64px); + max-width: 396px; margin-top: 8px; } diff --git a/src/components/pages/ABDesign/v1/components/Questionnaire/styles.module.css b/src/components/pages/ABDesign/v1/components/Questionnaire/styles.module.css index 2d5375e..e8f06ca 100644 --- a/src/components/pages/ABDesign/v1/components/Questionnaire/styles.module.css +++ b/src/components/pages/ABDesign/v1/components/Questionnaire/styles.module.css @@ -49,6 +49,7 @@ align-items: center; gap: 11px; margin-top: 28px; + z-index: 0; } .description { diff --git a/src/components/pages/ABDesign/v1/data/zodiacSignsInfo.ts b/src/components/pages/ABDesign/v1/data/zodiacSignsInfo.ts new file mode 100644 index 0000000..73f1386 --- /dev/null +++ b/src/components/pages/ABDesign/v1/data/zodiacSignsInfo.ts @@ -0,0 +1,152 @@ +import { IZodicSignsInfo } from "@/data"; + +export const zodiacSignsInfo: IZodicSignsInfo = { + male: [ + { + name: "Capricorn", + img: "/questionnaire-redesign/zodiacs/male/pdf.sex.CAPRICORN.MALE.webp", + description: + "The Capricorn male, with mountain goat tenacity, climbs life's peaks with disciplined dedication.", + }, + { + name: "Aquarius", + img: "/questionnaire-redesign/zodiacs/male/pdf.sex.AQUARIUS.MALE.webp", + description: + "Revolutionary in thought, the Aquarius male breaks boundaries, envisioning a brighter, unconventional tomorrow.", + }, + { + name: "Pisces", + img: "/questionnaire-redesign/zodiacs/male/pdf.sex.PISCES.MALE.webp", + description: + "Dreamy and empathetic, the Pisces male navigates realms of emotion, often expressing his soul through artistry.", + }, + { + name: "Aries", + img: "/questionnaire-redesign/zodiacs/male/pdf.sex.ARIES.MALE.webp", + description: + "The Aries male charges forward with unparalleled energy, always ready to conquer new frontiers.", + }, + { + name: "Taurus", + img: "/questionnaire-redesign/zodiacs/male/pdf.sex.TAURUS.MALE.webp", + description: + "The Taurus male values stability, often displaying a potent mix of resilience and sensuality.", + }, + { + name: "Gemini", + img: "/questionnaire-redesign/zodiacs/male/pdf.sex.GEMINI.MALE.webp", + description: + "Ever-curious, the Gemini male is a whirlwind of ideas, often switching between topics with excitement.", + }, + { + name: "Cancer", + img: "/questionnaire-redesign/zodiacs/male/pdf.sex.CANCER.MALE.webp", + description: + "Deeply intuitive, the Cancer male guards his emotional realm, drawing strength from familial bonds.", + }, + { + name: "Leo", + img: "/questionnaire-redesign/zodiacs/male/pdf.sex.LEO.MALE.webp", + description: + "With his regal demeanor, the Leo male has a magnetic charisma that demands the spotlight.", + }, + { + name: "Virgo", + img: "/questionnaire-redesign/zodiacs/male/pdf.sex.VIRGO.MALE.webp", + description: + "With an eye for detail, the Virgo male seeks perfection, often being the methodical problem solver in the room.", + }, + { + name: "Libra", + img: "/questionnaire-redesign/zodiacs/male/pdf.sex.LIBRA.MALE.webp", + description: + "Driven by harmony, the Libra male gracefully balances life's challenges, always seeking the middle ground.", + }, + { + name: "Scorpio", + img: "/questionnaire-redesign/zodiacs/male/pdf.sex.SCORPIO.MALE.webp", + description: + "The Scorpio male delves deep, with an intensity that can unravel life's mysteries, driven by passion and determination.", + }, + { + name: "Sagittarius", + img: "/questionnaire-redesign/zodiacs/male/pdf.sex.SAGITTARIUS.MALE.webp", + description: + "With wanderlust in his heart, the Sagittarius male chases knowledge and adventure, ever the eternal optimist.", + }, + ], + female: [ + { + name: "Capricorn", + img: "/questionnaire-redesign/zodiacs/female/pdf.sex.CAPRICORN.FEMALE.webp", + description: + "Grounded and wise, the Capricorn female stands as a pillar of resilience, merging ambition with purpose.", + }, + { + name: "Aquarius", + img: "/questionnaire-redesign/zodiacs/female/pdf.sex.AQUARIUS.FEMALE.webp", + description: + "The Aquarius female, with her avant-garde spirit, dances to her own rhythm, forever championing innovation.", + }, + { + name: "Pisces", + img: "/questionnaire-redesign/zodiacs/female/pdf.sex.PISCES.FEMALE.webp", + description: + "Ethereal and compassionate, the Pisces female feels deeply, weaving tales of romance and magic in her wake.", + }, + { + name: "Aries", + img: "/questionnaire-redesign/zodiacs/female/pdf.sex.ARIES.FEMALE.webp", + description: + "Radiating confidence, the Aries female often leads the pack, fueled by ambition and determination.", + }, + { + name: "Taurus", + img: "/questionnaire-redesign/zodiacs/female/pdf.sex.TAURUS.FEMALE.webp", + description: + "Grounded and graceful, the Taurus female appreciates the beauty and luxury in life, always seeking comfort.", + }, + { + name: "Gemini", + img: "/questionnaire-redesign/zodiacs/female/pdf.sex.GEMINI.FEMALE.webp", + description: + "Sparkling with wit, the Gemini female charms with her versatility, constantly adapting to change.", + }, + { + name: "Cancer", + img: "/questionnaire-redesign/zodiacs/female/pdf.sex.CANCER.FEMALE.webp", + description: + "The nurturing spirit of a Cancer female creates an embracing cocoon of comfort for loved ones.", + }, + { + name: "Leo", + img: "/questionnaire-redesign/zodiacs/female/pdf.sex.LEO.FEMALE.webp", + description: + "Vibrant and confident, the Leo female radiates warmth, ruling her domain with generosity and grace.", + }, + { + name: "Virgo", + img: "/questionnaire-redesign/zodiacs/female/pdf.sex.VIRGO.FEMALE.webp", + description: + "Discerning and diligent, the Virgo female navigates life with analytical prowess and a pure heart.", + }, + { + name: "Libra", + img: "/questionnaire-redesign/zodiacs/female/pdf.sex.LIBRA.FEMALE.webp", + description: + "Charm personified, the Libra female is the embodiment of elegance, wielding diplomacy with an artful touch.", + }, + { + name: "Scorpio", + img: "/questionnaire-redesign/zodiacs/female/pdf.sex.SCORPIO.FEMALE.webp", + description: + "The enigmatic Scorpio female possesses a magnetic allure, her depths veiling strength and vulnerability.", + }, + { + name: "Sagittarius", + img: "/questionnaire-redesign/zodiacs/female/pdf.sex.SAGITTARIUS.FEMALE.webp", + description: + "Vivacious and free-spirited, the Sagittarius female journeys through life, spreading joy and infectious enthusiasm.", + }, + ], +}; \ No newline at end of file diff --git a/src/components/pages/ABDesign/v1/pages/EmailEnterPage/index.tsx b/src/components/pages/ABDesign/v1/pages/EmailEnterPage/index.tsx index d480ef0..950db9b 100644 --- a/src/components/pages/ABDesign/v1/pages/EmailEnterPage/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/EmailEnterPage/index.tsx @@ -4,10 +4,8 @@ import { useNavigate, useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { actions, selectors } from "@/store"; -import { useApi } from "@/api"; import routes from "@/routes"; import NameInput from "./NameInput"; -import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans"; import Title from "@/components/Title"; import EmailInput from "./EmailInput"; import Policy from "@/components/Policy"; @@ -18,6 +16,8 @@ import { useDynamicSize } from "@/hooks/useDynamicSize"; import QuestionnaireGreenButton from "../../ui/GreenButton"; import { ESourceAuthorization } from "@/api/resources/User"; import { useAuthentication } from "@/hooks/authentication/use-authentication"; +import { usePaywall } from "@/hooks/paywall/usePaywall"; +import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall"; interface IEmailEnterPage { redirectUrl?: string; @@ -28,8 +28,7 @@ function EmailEnterPage({ redirectUrl = routes.client.emailConfirmV1(), isRequiredName = false, }: IEmailEnterPage): JSX.Element { - const api = useApi(); - const { t, i18n } = useTranslation(); + const { t } = useTranslation(); const dispatch = useDispatch(); const navigate = useNavigate(); const [email, setEmail] = useState(""); @@ -38,53 +37,33 @@ function EmailEnterPage({ const [isValidEmail, setIsValidEmail] = useState(false); const [isValidName, setIsValidName] = useState(!isRequiredName); const [isAuth, setIsAuth] = useState(false); - const [subPlans, setSubPlans] = useState([]); - const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan); - const [activeSubPlan, setActiveSubPlan] = useState( - activeSubPlanFromStore - ); - const locale = i18n.language; const { subPlan } = useParams(); const { width: pageWidth, elementRef: pageRef } = useDynamicSize({}); const { error, isLoading, authorization } = useAuthentication(); const { gender } = useSelector(selectors.selectQuestionnaire); + const activeProductFromStore = useSelector(selectors.selectActiveProduct); + const { products } = usePaywall({ + placementKey: EPlacementKeys["aura.placement.redesign.main"], + }); + const [activeProduct, setActiveProduct] = useState( + activeProductFromStore + ); useEffect(() => { if (subPlan) { - const targetSubPlan = subPlans.find( - (sub_plan) => + const targetProduct = products.find( + (product) => String( - sub_plan?.trial?.price_cents - ? Math.floor((sub_plan?.trial?.price_cents + 1) / 100) - : sub_plan.id.replace(".", "") + product?.trialPrice + ? Math.floor((product?.trialPrice + 1) / 100) + : product.key.replace(".", "") ) === subPlan ); - if (targetSubPlan) { - setActiveSubPlan(targetSubPlan); + if (targetProduct) { + setActiveProduct(targetProduct); } } - }, [subPlan, subPlans]); - - useEffect(() => { - (async () => { - const { sub_plans } = await api.getSubscriptionPlans({ locale }); - const plans = sub_plans - .filter((plan: ISubscriptionPlan) => plan.provider === "stripe") - .sort((a, b) => { - if (!a.trial || !b.trial) { - return 0; - } - if (a?.trial?.price_cents < b?.trial?.price_cents) { - return -1; - } - if (a?.trial?.price_cents > b?.trial?.price_cents) { - return 1; - } - return 0; - }); - setSubPlans(plans); - })(); - }, [api, locale]); + }, [subPlan, products]); const handleValidEmail = (email: string) => { dispatch(actions.form.addEmail(email)); @@ -129,7 +108,7 @@ function EmailEnterPage({ await authorization(email, source); dispatch( actions.payment.update({ - activeSubPlan, + activeProduct, }) ); setIsAuth(true); diff --git a/src/components/pages/ABDesign/v1/pages/Onboarding/index.tsx b/src/components/pages/ABDesign/v1/pages/Onboarding/index.tsx index cf78572..5508905 100644 --- a/src/components/pages/ABDesign/v1/pages/Onboarding/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/Onboarding/index.tsx @@ -5,6 +5,8 @@ import routes from "@/routes"; import { useNavigate } from "react-router-dom"; import { onboardingTitles } from "../../data/onboarding"; import ProgressBarLine from "@/components/ui/ProgressBarLine"; +import { usePaywall } from "@/hooks/paywall/usePaywall"; +import { EPlacementKeys } from "@/api/resources/Paywall"; function OnboardingPage() { const navigate = useNavigate(); @@ -14,6 +16,7 @@ function OnboardingPage() { const classNameTimeOut = useRef(); const [progress, setProgress] = useState(0); const progressInterval = useRef(); + usePaywall({ placementKey: EPlacementKeys["aura.placement.redesign.main"] }); const handleNext = useCallback(() => { navigate(routes.client.trialChoiceV1()); @@ -51,7 +54,11 @@ function OnboardingPage() { return (
- Leo + Leo {onboardingTitles[activeIndexTitle] && ( {onboardingTitles[activeIndexTitle]} diff --git a/src/components/pages/ABDesign/v1/pages/Onboarding/styles.module.css b/src/components/pages/ABDesign/v1/pages/Onboarding/styles.module.css index dc240e5..2219e09 100644 --- a/src/components/pages/ABDesign/v1/pages/Onboarding/styles.module.css +++ b/src/components/pages/ABDesign/v1/pages/Onboarding/styles.module.css @@ -16,6 +16,7 @@ margin-top: 100px; width: 100%; max-width: 273px; + transition: opacity 1s; } .title { diff --git a/src/components/pages/ABDesign/v1/pages/RelationshipZodiacInfo/index.tsx b/src/components/pages/ABDesign/v1/pages/RelationshipZodiacInfo/index.tsx index fd3bf31..11ba9ba 100644 --- a/src/components/pages/ABDesign/v1/pages/RelationshipZodiacInfo/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/RelationshipZodiacInfo/index.tsx @@ -61,12 +61,12 @@ function RelationshipZodiacInfoPage() { /> <div className={styles["image-container"]}> <img - src={`/questionnaire/zodiacs/${gender}/${zodiacSign?.toLowerCase()}.webp`} + src={`/questionnaire-redesign/zodiacs/${gender}/pdf.sex.${zodiacSign?.toUpperCase()}.${gender.toUpperCase()}.webp`} alt="The zodiac signs" /> <img src="/plus.svg" alt="Plus" /> <img - src={`/questionnaire/zodiacs/${partnerGender}/${partnerZodiacSign?.toLowerCase()}.webp`} + src={`/questionnaire-redesign/zodiacs/${partnerGender}/pdf.sex.${partnerZodiacSign?.toUpperCase()}.${partnerGender.toUpperCase()}.webp`} alt="The zodiac signs" /> </div> diff --git a/src/components/pages/ABDesign/v1/pages/RelationshipZodiacInfo/styles.module.css b/src/components/pages/ABDesign/v1/pages/RelationshipZodiacInfo/styles.module.css index fbcf33c..ca1b4ca 100644 --- a/src/components/pages/ABDesign/v1/pages/RelationshipZodiacInfo/styles.module.css +++ b/src/components/pages/ABDesign/v1/pages/RelationshipZodiacInfo/styles.module.css @@ -20,15 +20,17 @@ } .image-container { - display: flex; - justify-content: center; + display: grid; align-items: center; + justify-items: center; + grid-template-columns: calc(50% - 30px) min-content calc(50% - 30px); width: 100%; - gap: 32px; + gap: 6px; } .image-container > img { - max-width: 118px; + /* max-width: 118px; */ + max-height: 196px; } .compatibility-description-container { diff --git a/src/components/pages/ABDesign/v1/pages/SingleZodiacInfo/index.tsx b/src/components/pages/ABDesign/v1/pages/SingleZodiacInfo/index.tsx index d52b24d..8b4a402 100644 --- a/src/components/pages/ABDesign/v1/pages/SingleZodiacInfo/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/SingleZodiacInfo/index.tsx @@ -4,17 +4,18 @@ import { selectors } from "@/store"; import { getZodiacSignByDate } from "@/services/zodiac-sign"; import { useNavigate } from "react-router-dom"; import routes from "@/routes"; -import { IZodicSignsInfo, zodicSignsInfo } from "@/data"; +import { IZodicSignsInfo } from "@/data"; import Title from "@/components/Title"; import Header from "../../components/Header"; import QuestionnaireGreenButton from "../../ui/GreenButton"; +import { zodiacSignsInfo } from "../../data/zodiacSignsInfo"; function SingleZodiacInfoPage() { const navigate = useNavigate(); const birthdate = useSelector(selectors.selectQuestionnaire).birthdate; const gender = useSelector(selectors.selectQuestionnaire).gender; const zodiac = getZodiacSignByDate(birthdate); - const zodiacInfo = zodicSignsInfo[gender as keyof IZodicSignsInfo].find( + const zodiacInfo = zodiacSignsInfo[gender as keyof IZodicSignsInfo].find( (sign) => sign.name === zodiac ); diff --git a/src/components/pages/ABDesign/v1/pages/TrialChoice/index.tsx b/src/components/pages/ABDesign/v1/pages/TrialChoice/index.tsx index c4b75a7..03b7c99 100644 --- a/src/components/pages/ABDesign/v1/pages/TrialChoice/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/TrialChoice/index.tsx @@ -1,8 +1,5 @@ import styles from "./styles.module.css"; -import { useEffect, useMemo, useState } from "react"; -import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans"; -import { useTranslation } from "react-i18next"; -import { useApi } from "@/api"; +import { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { actions, selectors } from "@/store"; import { useNavigate } from "react-router-dom"; @@ -13,66 +10,32 @@ import BackgroundTopBlob from "../../ui/BackgroundTopBlob"; import { useDynamicSize } from "@/hooks/useDynamicSize"; import PriceList from "../../components/PriceList"; import QuestionnaireGreenButton from "../../ui/GreenButton"; - -interface IPlanKey { - [key: string]: number; -} +import { usePaywall } from "@/hooks/paywall/usePaywall"; +import { EPlacementKeys } from "@/api/resources/Paywall"; +import { getRandomArbitrary } from "@/services/random-value"; +import Loader from "@/components/Loader"; function TrialChoicePage() { - const { i18n } = useTranslation(); - const locale = i18n.language; - const api = useApi(); const dispatch = useDispatch(); const navigate = useNavigate(); const selectedPrice = useSelector(selectors.selectSelectedPrice); const homeConfig = useSelector(selectors.selectHome); const email = useSelector(selectors.selectEmail); - const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]); const [isDisabled, setIsDisabled] = useState(true); - const allowedPlans = useMemo(() => [""], []); + const [countUsers, setCountUsers] = useState(752); const { width: pageWidth, elementRef: pageRef } = useDynamicSize({}); const { gender } = useSelector(selectors.selectQuestionnaire); + const { products, isLoading, getText } = usePaywall({ + placementKey: EPlacementKeys["aura.placement.redesign.main"], + }); useEffect(() => { - (async () => { - const { sub_plans } = await api.getSubscriptionPlans({ locale }); - const plansWithoutTest = sub_plans.filter( - (plan: ISubscriptionPlan) => !plan.name.includes("(test)") - ); - const plansKeys: IPlanKey = {}; - const plans: ISubscriptionPlan[] = []; - for (const plan of plansWithoutTest) { - plansKeys[plan.name] = plansKeys[plan.name] - ? plansKeys[plan.name] + 1 - : 1; - if ( - (plansKeys[plan.name] > 1 && !plan.trial?.is_free && !!plan.trial) || - allowedPlans.includes(plan.id) - ) { - const targetPlan = plansWithoutTest.find( - (item) => item.name === plan.name && item.id.includes("stripe") - ); - plans.push(targetPlan as ISubscriptionPlan); - } - } - - plans.sort((a, b) => { - if (!a.trial || !b.trial) { - return 0; - } - if (a.trial?.price_cents < b.trial?.price_cents) { - return -1; - } - if (a.trial?.price_cents > b.trial?.price_cents) { - return 1; - } - return 0; - }); - - setSubPlans(plans); - })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [api, locale]); + const randomDelay = getRandomArbitrary(3000, 5000); + const countUsersTimeOut = setTimeout(() => { + setCountUsers((prevState) => prevState + 1); + }, randomDelay); + return () => clearTimeout(countUsersTimeOut); + }, [countUsers]); const handlePriceItem = () => { setIsDisabled(false); @@ -101,60 +64,76 @@ function TrialChoicePage() { height={180} /> <Header className={styles.header} /> - <p className={styles.text} style={{ marginTop: "60px" }}> - We've helped{" "} - <span className={styles.blue}> - <b>millions</b> - </span>{" "} - of people to have happier lives and better relationships, and we want to - help you too. - </p> - <p className={`${styles.text} ${styles.bold}`}> - Money shouldn’t stand in the way of finding astrology guidance that - finally works. So, choose an amount that you think is reasonable to try - us out for one week. - </p> - <p className={`${styles.text} ${styles.bold} ${styles.blue}`}> - It costs us $13.67 to offer a 3-day trial, but please choose the amount - you are comfortable with. - </p> - <div className={styles["price-container"]}> - <PriceList - subPlans={subPlans} - activeItem={selectedPrice} - classNameItem={styles["price-item"]} - classNameItemActive={`${styles["price-item-active"]} ${styles[gender]}`} - click={handlePriceItem} - /> - <p className={styles["auxiliary-text"]} style={{ maxWidth: "75%" }}> - This option will help us support those who need to select the lowest - trial prices! - </p> - <img - className={styles["arrow-image"]} - src="/arrow.svg" - alt={`Arrow to $${subPlans.at(-1)}`} - /> - </div> - <div className={styles["emails-list-container"]}> - <EmailsList - classNameContainer={`${styles["emails-container"]} ${styles[gender]}`} - classNameTitle={styles["emails-title"]} - classNameEmailItem={styles["email-item"]} - direction="right-left" - /> - </div> - <p className={styles.email}>{email}</p> - <QuestionnaireGreenButton - className={styles.button} - disabled={isDisabled} - onClick={handleNext} - > - See my plan - </QuestionnaireGreenButton> - <p className={styles["auxiliary-text"]}> - *Cost of trial as of February 2024 - </p> + {!isLoading && ( + <> + <p className={styles.text} style={{ marginTop: "60px" }}> + {getText("text.0", { + replacementSelector: "b", + color: "#1C38EA", + })} + </p> + <p className={`${styles.text} ${styles.bold}`}> + {getText("text.1", { + color: "#1C38EA", + })} + </p> + <p className={`${styles.text} ${styles.bold} ${styles.blue}`}> + {getText("text.2", { + color: "#1C38EA", + })} + </p> + <div className={styles["price-container"]}> + <PriceList + products={products} + activeItem={selectedPrice} + classNameItem={styles["price-item"]} + classNameItemActive={`${styles["price-item-active"]} ${styles[gender]}`} + click={handlePriceItem} + /> + <p className={styles["auxiliary-text"]} style={{ maxWidth: "75%" }}> + {getText("text.3", { + color: "#1C38EA", + })} + </p> + <img + className={styles["arrow-image"]} + src="/arrow.svg" + alt={`Arrow to $${products.at(-1)?.trialPrice}`} + /> + </div> + <div className={styles["emails-list-container"]}> + <EmailsList + title={getText("text.5", { + replacementSelector: "strong", + replacement: { + target: "${quantity}", + replacement: countUsers.toString(), + }, + })} + classNameContainer={`${styles["emails-container"]} ${styles[gender]}`} + classNameTitle={styles["emails-title"]} + classNameEmailItem={styles["email-item"]} + direction="right-left" + /> + </div> + <p className={styles.email}>{email}</p> + <QuestionnaireGreenButton + className={styles.button} + disabled={isDisabled} + onClick={handleNext} + > + {getText("text.button.1", { + color: "#1C38EA", + })} + </QuestionnaireGreenButton> + <p className={styles["auxiliary-text"]}> + {getText("text.4", { + color: "#1C38EA", + })} + </p> + </> + )} + {isLoading && <Loader className={styles.loader} />} </section> ); } diff --git a/src/components/pages/ABDesign/v1/pages/TrialChoice/styles.module.css b/src/components/pages/ABDesign/v1/pages/TrialChoice/styles.module.css index 83e944b..b5aec72 100644 --- a/src/components/pages/ABDesign/v1/pages/TrialChoice/styles.module.css +++ b/src/components/pages/ABDesign/v1/pages/TrialChoice/styles.module.css @@ -137,4 +137,11 @@ .email { font-weight: 500; +} + +.loader { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); } \ No newline at end of file diff --git a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/Header/index.tsx b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/Header/index.tsx index 0616dff..1a75670 100644 --- a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/Header/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/Header/index.tsx @@ -1,6 +1,8 @@ +import { useSelector } from "react-redux"; import CustomButton from "../CustomButton"; import DiscountExpires from "../DiscountExpires"; import styles from "./styles.module.css"; +import { selectors } from "@/store"; interface IHeaderProps { buttonText?: string; @@ -13,8 +15,14 @@ function Header({ buttonText = "get my reading", buttonClassName = "", }: IHeaderProps) { + const { gender } = useSelector(selectors.selectQuestionnaire); return ( - <header className={styles.header}> + <header + className={styles.header} + style={{ + backgroundColor: gender === "male" ? "#C1E5FF" : "#F7EBFF", + }} + > <DiscountExpires /> <CustomButton className={`${styles.button} ${buttonClassName}`} diff --git a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/Header/styles.module.css b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/Header/styles.module.css index 2d247f5..bd059bc 100644 --- a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/Header/styles.module.css +++ b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/Header/styles.module.css @@ -1,5 +1,7 @@ .header { - position: relative; + position: sticky; + top: 0; + z-index: 30; height: 62px; width: 100%; max-width: 560px; diff --git a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PaymentModal/index.tsx b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PaymentModal/index.tsx index 660ecc4..cdb78ad 100644 --- a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PaymentModal/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PaymentModal/index.tsx @@ -4,53 +4,61 @@ import PaymentMethodsChoice from "../PaymentMethodsChoice"; import { useEffect, useMemo, useState } from "react"; import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods"; import { Elements } from "@stripe/react-stripe-js"; -import ApplePayButton from "@/components/StripePage/ApplePayButton"; -import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm"; +import ApplePayButton from "@/components/PaymentPage/methods/ApplePayButton"; +import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm"; import { Stripe, loadStripe } from "@stripe/stripe-js"; import { useSelector } from "react-redux"; import { selectors } from "@/store"; -import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans"; -import { useApi } from "@/api"; import { useNavigate } from "react-router-dom"; import routes from "@/routes"; -import { useTranslation } from "react-i18next"; -import { useAuth } from "@/auth"; import Loader from "@/components/Loader"; -import { getPriceFromTrial } from "@/services/price"; import SecurityPayments from "../SecurityPayments"; +import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall"; +import { usePaywall } from "@/hooks/paywall/usePaywall"; +import { useMakePayment } from "@/hooks/payment/useMakePayment"; interface IPaymentModalProps { - activeSubscriptionPlan?: ISubscriptionPlan; + activeProduct?: IPaywallProduct; noTrial?: boolean; returnUrl?: string; + placementKey?: EPlacementKeys; } +const getPrice = (product: IPaywallProduct) => { + return (product.trialPrice || 0) / 100; +}; + function PaymentModal({ - activeSubscriptionPlan, + activeProduct, noTrial, returnUrl, + placementKey = EPlacementKeys["aura.placement.redesign.main"], }: IPaymentModalProps) { - const { i18n } = useTranslation(); - const locale = i18n.language; - const api = useApi(); - const { token } = useAuth(); const navigate = useNavigate(); - const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan); - const activeSubPlan = activeSubscriptionPlan - ? activeSubscriptionPlan - : activeSubPlanFromStore; const [stripePromise, setStripePromise] = useState<Promise<Stripe | null> | null>(null); - const [clientSecret, setClientSecret] = useState<string>(""); - const [subscriptionReceiptId, setSubscriptionReceiptId] = - useState<string>(""); - const [isLoading, setIsLoading] = useState(true); - const [isError, setIsError] = useState<boolean>(false); + + const activeProductFromStore = useSelector(selectors.selectActiveProduct); + const _activeProduct = activeProduct ? activeProduct : activeProductFromStore; + const { + paymentIntentId, + clientSecret, + returnUrl: checkoutUrl, + paymentType, + publicKey, + isLoading, + error, + } = useMakePayment({ + productId: _activeProduct?._id || "", + returnPaidUrl: + returnUrl + }); + + if (checkoutUrl?.length) { + window.location.href = checkoutUrl; + } const paymentMethodsButtons = useMemo(() => { - // return paymentMethods.filter( - // (method) => method.id !== EPaymentMethod.PAYMENT_BUTTONS - // ); return paymentMethods; }, []); @@ -58,49 +66,24 @@ function PaymentModal({ EPaymentMethod.PAYMENT_BUTTONS ); + const { products } = usePaywall({ placementKey }); + const onSelectPaymentMethod = (method: EPaymentMethod) => { setSelectedPaymentMethod(method); }; useEffect(() => { (async () => { - const siteConfig = await api.getAppConfig({ bundleId: "auraweb" }); - setStripePromise(loadStripe(siteConfig.data.stripe_public_key)); - const { sub_plans } = await api.getSubscriptionPlans({ locale }); - const isActiveSubPlan = sub_plans.find( - (subPlan) => subPlan.id === activeSubPlan?.id + if (!products?.length || !publicKey) return; + setStripePromise(loadStripe(publicKey)); + const isActiveProduct = products.find( + (product) => product._id === _activeProduct?._id ); - if (!activeSubPlan || !isActiveSubPlan) { - navigate(routes.client.priceList()); + if (!_activeProduct || !isActiveProduct) { + navigate(routes.client.trialChoiceV1()); } })(); - }, [activeSubPlan, api, locale, navigate]); - - useEffect(() => { - (async () => { - try { - const { subscription_receipt } = await api.createSubscriptionReceipt({ - token, - way: "stripe", - subscription_receipt: { - sub_plan_id: activeSubPlan?.id || "stripe.7", - }, - }); - const { id } = subscription_receipt; - const { client_secret } = subscription_receipt.data; - const { checkout_url } = subscription_receipt.data; - if (checkout_url?.length) { - window.location.href = checkout_url; - } - setSubscriptionReceiptId(id); - setClientSecret(client_secret); - setIsLoading(false); - } catch (error) { - console.error(error); - setIsError(true); - } - })(); - }, [activeSubPlan?.id, api, token]); + }, [_activeProduct, navigate, products, publicKey]); if (isLoading) { return ( @@ -112,7 +95,7 @@ function PaymentModal({ ); } - if (isError) { + if (error?.length) { return ( <div className={styles["payment-modal"]}> <Title variant="h3" className={styles.title}> @@ -132,16 +115,13 @@ function PaymentModal({ selectedPaymentMethod={selectedPaymentMethod} onSelectPaymentMethod={onSelectPaymentMethod} /> - {activeSubPlan && ( + {_activeProduct && ( <div> {!noTrial && ( <> <p className={styles["sub-plan-description"]}> You will be charged only{" "} - <b> - ${getPriceFromTrial(activeSubPlan?.trial)} for your 3-day - trial. - </b> + <b>${getPrice(_activeProduct)} for your 3-day trial.</b> </p> <p className={styles["sub-plan-description"]}> We`ll <b>email you a reminder</b> before your trial period ends. @@ -160,16 +140,17 @@ function PaymentModal({ {selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && ( <div className={styles["payment-method"]}> <ApplePayButton - activeSubPlan={activeSubPlan} + activeProduct={_activeProduct} client_secret={clientSecret} - subscriptionReceiptId={subscriptionReceiptId} + subscriptionReceiptId={paymentIntentId} /> </div> )} {selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && ( <CheckoutForm - subscriptionReceiptId={subscriptionReceiptId} + confirmType={paymentType} + subscriptionReceiptId={paymentIntentId} returnUrl={returnUrl} /> )} diff --git a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PaymentTable/index.tsx b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PaymentTable/index.tsx index 8ccfc78..db836e4 100644 --- a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PaymentTable/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PaymentTable/index.tsx @@ -1,19 +1,22 @@ import Title from "@/components/Title"; import styles from "./styles.module.css"; -import { getPriceFromTrial } from "@/services/price"; -import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans"; import CustomButton from "../CustomButton"; import GuardPayments from "../GuardPayments"; import { useState } from "react"; import FullScreenModal from "@/components/FullScreenModal"; +import { IPaywallProduct } from "@/api/resources/Paywall"; interface IPaymentTableProps { - subPlan: ISubscriptionPlan; + product: IPaywallProduct; gender: string; buttonClick: () => void; } -function PaymentTable({ gender, subPlan, buttonClick }: IPaymentTableProps) { +const getPrice = (product: IPaywallProduct) => { + return (product.trialPrice || 0) / 100; +}; + +function PaymentTable({ gender, product, buttonClick }: IPaymentTableProps) { const [isOpenPrivacyModal, setIsOpenPrivacyModal] = useState<boolean>(false); const handleSubscriptionPolicyClick = (event: React.MouseEvent) => { event.preventDefault(); @@ -50,20 +53,18 @@ function PaymentTable({ gender, subPlan, buttonClick }: IPaymentTableProps) { <div className={styles["table-container"]}> <Title variant="h3" className={styles.title}> Personalized reading for{" "} - <span className={styles.purple}> - ${getPriceFromTrial(subPlan?.trial)} - </span> + <span className={styles.purple}>${getPrice(product)}</span>

Total today:

- ${getPriceFromTrial(subPlan?.trial)} + ${getPrice(product)}

Your cost per 2 weeks after trial

$65 - ${subPlan.price_cents / 100} + ${product.trialPrice / 100}
@@ -75,9 +76,9 @@ function PaymentTable({ gender, subPlan, buttonClick }: IPaymentTableProps) {

You are enrolling in 2 weeks subscription. By continuing you agree that if you don't cancel prior to the end of the 3-day trial for the $ - {getPriceFromTrial(subPlan?.trial)} you will automatically be charged - $19 every 2 weeks until you cancel in settings. Learn more about - cancellation and refund policy in{" "} + {getPrice(product)} you will automatically be charged $19 every 2 weeks + until you cancel in settings. Learn more about cancellation and refund + policy in{" "} Subscription policy

diff --git a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PersonalInformation/index.tsx b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PersonalInformation/index.tsx index 247479c..5b84af4 100644 --- a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PersonalInformation/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PersonalInformation/index.tsx @@ -22,7 +22,7 @@ function PersonalInformation({ >
{`${gender}
diff --git a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/WithPartnerInformation/index.tsx b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/WithPartnerInformation/index.tsx index 0ca186d..3af0731 100644 --- a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/WithPartnerInformation/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/WithPartnerInformation/index.tsx @@ -33,14 +33,15 @@ function WithPartnerInformation(props: IWithPartnerInformationProps) {
{`${gender}

You

+ Plus
{`${partnerGender}

Partner

diff --git a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/WithPartnerInformation/styles.module.css b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/WithPartnerInformation/styles.module.css index 634a673..259ade7 100644 --- a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/WithPartnerInformation/styles.module.css +++ b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/WithPartnerInformation/styles.module.css @@ -17,8 +17,10 @@ min-height: 100px; border-top-left-radius: 15px; border-top-right-radius: 15px; - display: flex; - justify-content: space-around; + display: grid; + grid-template-columns: calc(50% - 28px) min-content calc(50% - 28px); + align-items: center; + gap: 4px; padding-top: 10px; padding-bottom: 6px; } @@ -33,6 +35,8 @@ .image-container > img { height: 196px; + width: 100%; + object-fit: contain; } .image-container > p { diff --git a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/YourReading/index.tsx b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/YourReading/index.tsx index 3ddb4d8..6dbb855 100644 --- a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/YourReading/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/YourReading/index.tsx @@ -40,7 +40,7 @@ function YourReading({
{`${gender}
diff --git a/src/components/pages/ABDesign/v1/pages/TrialPayment/index.tsx b/src/components/pages/ABDesign/v1/pages/TrialPayment/index.tsx index 72073e2..d31907a 100644 --- a/src/components/pages/ABDesign/v1/pages/TrialPayment/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/TrialPayment/index.tsx @@ -12,11 +12,7 @@ import YourReading from "./components/YourReading"; import Reviews from "./components/Reviews"; import PointsList from "./components/PointsList"; import OftenAsk from "./components/OftenAsk"; -import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans"; import { useEffect, useState } from "react"; -import { useApi } from "@/api"; -import { getClientLocale } from "@/locales"; -import { Locale } from "@/components/PaymentTable"; import WithPartnerInformation from "./components/WithPartnerInformation"; import Modal from "@/components/Modal"; import PaymentModal from "./components/PaymentModal"; @@ -26,12 +22,11 @@ import TrialPaymentHeader from "./components/Header"; import Header from "../../components/Header"; import BackgroundTopBlob from "../../ui/BackgroundTopBlob"; import { useDynamicSize } from "@/hooks/useDynamicSize"; - -const locale = getClientLocale() as Locale; +import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall"; +import { usePaywall } from "@/hooks/paywall/usePaywall"; function TrialPaymentPage() { const dispatch = useDispatch(); - const api = useApi(); const navigate = useNavigate(); const birthdate = useSelector(selectors.selectBirthdate); const zodiacSign = getZodiacSignByDate(birthdate); @@ -46,10 +41,12 @@ function TrialPaymentPage() { flowChoice, } = useSelector(selectors.selectQuestionnaire); const partnerZodiacSign = getZodiacSignByDate(partnerBirthdate); - const [subPlans, setSubPlans] = useState([]); - const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan); - const [activeSubPlan, setActiveSubPlan] = useState( - activeSubPlanFromStore + const { products } = usePaywall({ + placementKey: EPlacementKeys["aura.placement.redesign.main"], + }); + const activeProductFromStore = useSelector(selectors.selectActiveProduct); + const [activeProduct, setActiveProduct] = useState( + activeProductFromStore ); const [isOpenPaymentModal, setIsOpenPaymentModal] = useState(false); const [singleOrWithPartner, setSingleOrWithPartner] = useState< @@ -57,43 +54,32 @@ function TrialPaymentPage() { >("single"); const { subPlan } = useParams(); - useEffect(() => { - (async () => { - const { sub_plans } = await api.getSubscriptionPlans({ locale }); - const plans = sub_plans - .filter((plan: ISubscriptionPlan) => plan.provider === "stripe") - .sort((a, b) => { - if (!a.trial || !b.trial) { - return 0; - } - if (a?.trial?.price_cents < b?.trial?.price_cents) { - return -1; - } - if (a?.trial?.price_cents > b?.trial?.price_cents) { - return 1; - } - return 0; - }); - setSubPlans(plans); - })(); - }, [api]); - useEffect(() => { if (subPlan) { - const targetSubPlan = subPlans.find( - (sub_plan) => + const targetProduct = products.find( + (product) => String( - sub_plan?.trial?.price_cents - ? Math.floor((sub_plan?.trial?.price_cents + 1) / 100) - : sub_plan.id.replace(".", "") + product?.trialPrice + ? Math.floor((product?.trialPrice + 1) / 100) + : product.key.replace(".", "") ) === subPlan ); - if (targetSubPlan) { - setActiveSubPlan(targetSubPlan); - dispatch(actions.payment.update({ activeSubPlan: targetSubPlan })); + if (targetProduct) { + setActiveProduct(targetProduct); + dispatch(actions.payment.update({ activeProduct })); } } - }, [dispatch, subPlan, subPlans]); + }, [dispatch, subPlan, products, activeProduct]); + + useEffect(() => { + if (!products.length) return; + const isActiveProduct = products.find( + (product) => product._id === activeProduct?._id + ); + if (!activeProduct || !isActiveProduct) { + navigate(routes.client.trialChoiceV1()); + } + }, [activeProduct, navigate, products]); useEffect(() => { if (["relationship", "married"].includes(flowChoice)) { @@ -103,12 +89,12 @@ function TrialPaymentPage() { setSingleOrWithPartner("single"); }, [flowChoice]); - if (!activeSubPlan) { - return ; + if (!activeProduct) { + return ; } if (!birthdate || !gender || !birthPlace) { - return ; + return ; } const handleDiscount = () => { @@ -168,7 +154,7 @@ function TrialPaymentPage() { diff --git a/src/components/pages/ABDesign/v1/pages/TrialPayment/styles.module.css b/src/components/pages/ABDesign/v1/pages/TrialPayment/styles.module.css index dc01c42..557d3ce 100644 --- a/src/components/pages/ABDesign/v1/pages/TrialPayment/styles.module.css +++ b/src/components/pages/ABDesign/v1/pages/TrialPayment/styles.module.css @@ -6,6 +6,8 @@ padding-bottom: 62px; width: 100%; max-width: 460px; + overflow: inherit; + overflow-x: clip; } .title { diff --git a/src/components/pages/ABDesign/v1/pages/TrialPaymentWithDiscount/PaymentDiscountTable/index.tsx b/src/components/pages/ABDesign/v1/pages/TrialPaymentWithDiscount/PaymentDiscountTable/index.tsx index a18d5ee..3157556 100644 --- a/src/components/pages/ABDesign/v1/pages/TrialPaymentWithDiscount/PaymentDiscountTable/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/TrialPaymentWithDiscount/PaymentDiscountTable/index.tsx @@ -2,10 +2,17 @@ import Title from "@/components/Title"; import styles from "./styles.module.css"; import { useSelector } from "react-redux"; import { selectors } from "@/store"; -import { getPriceFromTrial } from "@/services/price"; +import { IPaywallProduct } from "@/api/resources/Paywall"; + +const getPrice = (product: IPaywallProduct | null) => { + if (!product) { + return 0; + } + return (product.trialPrice === 100 ? 99 : product.trialPrice || 0) / 100; +}; function PaymentDiscountTable() { - const activeSub = useSelector(selectors.selectActiveSubPlan); + const activeProduct = useSelector(selectors.selectActiveProduct); return (
@@ -15,7 +22,11 @@ function PaymentDiscountTable() {

No pressure. Cancel anytime.

- Present + Present

Secret discount applied!

@@ -27,14 +38,14 @@ function PaymentDiscountTable() {

Your cost per 14 days after trial:

$19 - $9 + ${(activeProduct?.price || 0) / 100}

You save $30


Total today:

- {activeSub && ${getPriceFromTrial(activeSub.trial)}} + {activeProduct && ${getPrice(activeProduct)}}
); diff --git a/src/components/pages/ABDesign/v1/pages/TrialPaymentWithDiscount/index.tsx b/src/components/pages/ABDesign/v1/pages/TrialPaymentWithDiscount/index.tsx index 4122374..129d71f 100644 --- a/src/components/pages/ABDesign/v1/pages/TrialPaymentWithDiscount/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/TrialPaymentWithDiscount/index.tsx @@ -4,19 +4,44 @@ import MainButton from "@/components/MainButton"; import PaymentDiscountTable from "./PaymentDiscountTable"; import Modal from "@/components/Modal"; import PaymentModal from "../TrialPayment/components/PaymentModal"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { actions, selectors } from "@/store"; +import { useDispatch, useSelector } from "react-redux"; +import { usePaywall } from "@/hooks/paywall/usePaywall"; +import { EPlacementKeys } from "@/api/resources/Paywall"; function TrialPaymentWithDiscount() { + const dispatch = useDispatch(); + const { products } = usePaywall({ + placementKey: EPlacementKeys["aura.placement.secret.discount"], + }); + const productFromStore = useSelector(selectors.selectActiveProduct); const [isOpenPaymentModal, setIsOpenPaymentModal] = useState(false); const handleClose = () => { setIsOpenPaymentModal(false); }; + useEffect(() => { + if (!products.length) return; + const activeProduct = products.find( + (p) => p.trialPrice === productFromStore?.trialPrice + ); + if (!activeProduct) { + dispatch(actions.payment.update({ activeProduct: products[0] })); + } + if (activeProduct) { + dispatch(actions.payment.update({ activeProduct })); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dispatch, products]); + return (
- +

By continuing you agree that if you don't cancel prior to the end of the - 3-days trial, you will automatically be charged $9 for the introductory - period of 14 days thereafter the standard rate of $9 every 14 days until - you cancel in settings. Learn more about cancellation and refund policy - in Subscription terms. + 3-days trial, you will automatically be charged $ + {(productFromStore?.price || 0) / 100} for the introductory period of 14 + days thereafter the standard rate of $ + {(productFromStore?.price || 0) / 100} every 14 days until you cancel in + settings. Learn more about cancellation and refund policy in + Subscription terms.

); diff --git a/src/components/pages/AdditionalPurchases/pages/AddConsultation/index.tsx b/src/components/pages/AdditionalPurchases/pages/AddConsultation/index.tsx index b194f3e..509dbed 100644 --- a/src/components/pages/AdditionalPurchases/pages/AddConsultation/index.tsx +++ b/src/components/pages/AdditionalPurchases/pages/AddConsultation/index.tsx @@ -10,12 +10,10 @@ import { SinglePayment, useApi, useApiCall } from "@/api"; import { useSelector } from "react-redux"; import { selectors } from "@/store"; import { useCallback, useState } from "react"; -import { - ResponsePost, -} from "@/api/resources/SinglePayment"; +import { ResponsePost } from "@/api/resources/SinglePayment"; import { createSinglePayment } from "@/services/singlePayment"; import Modal from "@/components/Modal"; -import PaymentForm from "@/components/pages/PaymentWithEmailPage/PaymentForm"; +import PaymentForm from "@/components/pages/SinglePaymentPage/PaymentForm"; import { getPriceCentsToDollars } from "@/services/price"; import Loader, { LoaderColor } from "@/components/Loader"; @@ -25,9 +23,7 @@ function AddConsultationPage() { const api = useApi(); const tokenFromStore = useSelector(selectors.selectToken); const [isLoading, setIsLoading] = useState(false); - const [paymentIntent, setPaymentIntent] = useState< - ResponsePost | null - >(null); + const [paymentIntent, setPaymentIntent] = useState(null); const [isError, setIsError] = useState(false); const returnUrl = `${window.location.protocol}//${ window.location.host @@ -45,29 +41,34 @@ function AddConsultationPage() { ); const handleClick = async () => { - if (!userFromStore || !currentProduct) return; - setIsLoading(true); - const { productId, key } = currentProduct; - const paymentInfo = { - productId, - key, - }; - const paymentIntent = await createSinglePayment( - userFromStore, - paymentInfo, - tokenFromStore, - userFromStore.email, - userFromStore.profile.full_name, - userFromStore.profile.birthday, - returnUrl, - api - ); - setPaymentIntent(paymentIntent); - setIsLoading(false); - if ("payment" in paymentIntent) { - if (paymentIntent.payment.status === "paid") - return navigate(routes.client.getInformationPartner()); - return setIsError(true); + try { + if (!userFromStore || !currentProduct) return; + setIsLoading(true); + const { _id, key } = currentProduct; + const paymentInfo = { + productId: _id, + key, + }; + const paymentIntent = await createSinglePayment( + userFromStore, + paymentInfo, + tokenFromStore, + userFromStore.email, + userFromStore.profile.full_name, + userFromStore.profile.birthday, + returnUrl, + api + ); + setPaymentIntent(paymentIntent); + if ("payment" in paymentIntent) { + if (paymentIntent.payment.status === "paid") + return navigate(routes.client.getInformationPartner()); + return setIsError(true); + } + } catch (error) { + setIsError(true); + } finally { + setIsLoading(false); } }; @@ -86,7 +87,7 @@ function AddConsultationPage() { onClose={() => setPaymentIntent(null)} > - {getPriceCentsToDollars(currentProduct?.amount || 0)}$ + {getPriceCentsToDollars(currentProduct?.price || 0)}$ (null); + const [paymentIntent, setPaymentIntent] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); const [activeOffer, setActiveOffer] = useState(signUpOffers[0]); @@ -48,31 +44,36 @@ function AddReportPage() { }; const handleClick = async () => { - if (!userFromStore || !activeOffer) return; - const currentProduct = getCurrentProduct(activeOffer?.productKey); - if (!currentProduct) return; - setIsLoading(true); - const { productId, key } = currentProduct; - const paymentInfo = { - productId, - key, - }; - const paymentIntent = await createSinglePayment( - userFromStore, - paymentInfo, - tokenFromStore, - userFromStore.email, - userFromStore.profile.full_name, - userFromStore.profile.birthday, - returnUrl, - api - ); - setPaymentIntent(paymentIntent); - setIsLoading(false); - if ("payment" in paymentIntent) { - if (paymentIntent.payment.status === "paid") - return navigate(routes.client.unlimitedReadings()); - return setIsError(true); + try { + if (!userFromStore || !activeOffer) return; + const currentProduct = getCurrentProduct(activeOffer?.productKey); + if (!currentProduct) return; + setIsLoading(true); + const { _id, key } = currentProduct; + const paymentInfo = { + productId: _id, + key, + }; + const paymentIntent = await createSinglePayment( + userFromStore, + paymentInfo, + tokenFromStore, + userFromStore.email, + userFromStore.profile.full_name, + userFromStore.profile.birthday, + returnUrl, + api + ); + setPaymentIntent(paymentIntent); + if ("payment" in paymentIntent) { + if (paymentIntent.payment.status === "paid") + return navigate(routes.client.unlimitedReadings()); + return setIsError(true); + } + } catch (error) { + setIsError(true); + } finally { + setIsLoading(false); } }; diff --git a/src/components/pages/AdditionalPurchases/pages/UnlimitedReadings/index.tsx b/src/components/pages/AdditionalPurchases/pages/UnlimitedReadings/index.tsx index 0d02ab3..7d638a7 100644 --- a/src/components/pages/AdditionalPurchases/pages/UnlimitedReadings/index.tsx +++ b/src/components/pages/AdditionalPurchases/pages/UnlimitedReadings/index.tsx @@ -16,12 +16,10 @@ import { selectors } from "@/store"; import { useCallback, useState } from "react"; import { createSinglePayment } from "@/services/singlePayment"; import Loader, { LoaderColor } from "@/components/Loader"; -import { - ResponsePost, -} from "@/api/resources/SinglePayment"; +import { ResponsePost } from "@/api/resources/SinglePayment"; import Modal from "@/components/Modal"; import { getPriceCentsToDollars } from "@/services/price"; -import PaymentForm from "@/components/pages/PaymentWithEmailPage/PaymentForm"; +import PaymentForm from "@/components/pages/SinglePaymentPage/PaymentForm"; const sliderSettings = { dots: false, @@ -40,9 +38,7 @@ function UnlimitedReadingsPage() { const api = useApi(); const tokenFromStore = useSelector(selectors.selectToken); const [isLoading, setIsLoading] = useState(false); - const [paymentIntent, setPaymentIntent] = useState< - ResponsePost | null - >(null); + const [paymentIntent, setPaymentIntent] = useState(null); const [isError, setIsError] = useState(false); const returnUrl = `${window.location.protocol}//${ window.location.host @@ -60,29 +56,34 @@ function UnlimitedReadingsPage() { ); const handleClick = async () => { - if (!userFromStore || !currentProduct) return; - setIsLoading(true); - const { productId, key } = currentProduct; - const paymentInfo = { - productId, - key, - }; - const paymentIntent = await createSinglePayment( - userFromStore, - paymentInfo, - tokenFromStore, - userFromStore.email, - userFromStore.profile.full_name, - userFromStore.profile.birthday, - returnUrl, - api - ); - setPaymentIntent(paymentIntent); - setIsLoading(false); - if ("payment" in paymentIntent) { - if (paymentIntent.payment.status === "paid") - return navigate(routes.client.addConsultation()); - return setIsError(true); + try { + if (!userFromStore || !currentProduct) return; + setIsLoading(true); + const { _id, key } = currentProduct; + const paymentInfo = { + productId: _id, + key, + }; + const paymentIntent = await createSinglePayment( + userFromStore, + paymentInfo, + tokenFromStore, + userFromStore.email, + userFromStore.profile.full_name, + userFromStore.profile.birthday, + returnUrl, + api + ); + setPaymentIntent(paymentIntent); + if ("payment" in paymentIntent) { + if (paymentIntent.payment.status === "paid") + return navigate(routes.client.addConsultation()); + return setIsError(true); + } + } catch (error) { + setIsError(true); + } finally { + setIsLoading(false); } }; @@ -102,7 +103,7 @@ function UnlimitedReadingsPage() { onClose={() => setPaymentIntent(null)} > - {getPriceCentsToDollars(currentProduct?.amount || 0)}$ + {getPriceCentsToDollars(currentProduct?.price || 0)}$
@@ -11,7 +17,7 @@ function ComparePrices() {
-

up to $13.67

+

{oldPrice}

@@ -22,7 +28,7 @@ function ComparePrices() {
-

$0

+

${newPrice}

diff --git a/src/components/pages/EmailLetters/MarketingLanding/components/SpecialOfferBanner/index.tsx b/src/components/pages/EmailLetters/MarketingLanding/components/SpecialOfferBanner/index.tsx index e2ae749..219d06c 100644 --- a/src/components/pages/EmailLetters/MarketingLanding/components/SpecialOfferBanner/index.tsx +++ b/src/components/pages/EmailLetters/MarketingLanding/components/SpecialOfferBanner/index.tsx @@ -1,13 +1,13 @@ import Title from "@/components/Title"; import styles from "./styles.module.css"; -function SpecialOfferBanner() { +function SpecialOfferBanner({ title }: { title: string }) { return (
Wrapped Gift
- Special Offer! + {title}

Everything for free. Trial include!

diff --git a/src/components/pages/EmailLetters/MarketingLanding/index.tsx b/src/components/pages/EmailLetters/MarketingLanding/index.tsx index 902cb27..919bf20 100644 --- a/src/components/pages/EmailLetters/MarketingLanding/index.tsx +++ b/src/components/pages/EmailLetters/MarketingLanding/index.tsx @@ -15,11 +15,16 @@ import { selectors } from "@/store"; import { getZodiacSignByDate } from "@/services/zodiac-sign"; import { useNavigate } from "react-router-dom"; import routes from "@/routes"; +import { usePaywall } from "@/hooks/paywall/usePaywall"; +import { EPlacementKeys } from "@/api/resources/Paywall"; function MarketingLanding() { const birthdate = useSelector(selectors.selectBirthdate); const zodiacSign = getZodiacSignByDate(birthdate); const navigate = useNavigate(); + const { paywall, products, getText } = usePaywall({ + placementKey: EPlacementKeys["aura.placement.email.marketing"], + }); const handleNext = () => { navigate(routes.client.email("marketing-trial-payment")); @@ -27,7 +32,7 @@ function MarketingLanding() { return (
- +
Hey, {zodiacSign} Sun 👋 @@ -66,7 +71,10 @@ function MarketingLanding() { alt="Understanding" style={{ minHeight: "323px" }} /> - <ComparePrices /> + <ComparePrices + oldPrice={getText("text.old.price", {}) as string} + newPrice={`${((products[0]?.trialPrice || 0) / 100).toFixed(2)}`} + /> <PointsList points={marketingLandingPointsList} title="Your plan also includes:" diff --git a/src/components/pages/EmailLetters/MarketingTrialPayment/index.tsx b/src/components/pages/EmailLetters/MarketingTrialPayment/index.tsx index fbc5f27..5cbfb28 100644 --- a/src/components/pages/EmailLetters/MarketingTrialPayment/index.tsx +++ b/src/components/pages/EmailLetters/MarketingTrialPayment/index.tsx @@ -5,29 +5,22 @@ import MainButton from "@/components/MainButton"; import Modal from "@/components/Modal"; import PaymentModal from "../../TrialPayment/components/PaymentModal"; import { useEffect, useState } from "react"; -import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans"; -import { useApi } from "@/api"; -import { useTranslation } from "react-i18next"; +import { usePaywall } from "@/hooks/paywall/usePaywall"; +import { EPlacementKeys } from "@/api/resources/Paywall"; +import { useDispatch } from "react-redux"; +import { actions } from "@/store"; function MarketingTrialPayment() { - const { i18n } = useTranslation(); - const locale = i18n.language; - const api = useApi(); + const dispatch = useDispatch(); const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false); - const [freeTrialPlan, setFreeTrialPlan] = useState< - ISubscriptionPlan | undefined - >(); - // get free trial plan + const { products } = usePaywall({ + placementKey: EPlacementKeys["aura.placement.email.marketing"], + }); + useEffect(() => { - (async () => { - const { sub_plans } = await api.getSubscriptionPlans({ locale }); - const _freeTrialPlan = sub_plans.find( - (subPlan) => subPlan.trial?.is_free - ); - setFreeTrialPlan(_freeTrialPlan); - })(); - }, [api, locale]); + dispatch(actions.payment.update({ activeProduct: products[0] })); + }, [dispatch, products]); const openStripeModal = () => { setIsOpenPaymentModal(true); @@ -39,13 +32,17 @@ function MarketingTrialPayment() { return ( <> - <Modal - containerClassName={styles.modal} - open={isOpenPaymentModal} - onClose={handleCloseModal} - > - <PaymentModal activeSubscriptionPlan={freeTrialPlan} /> - </Modal> + {products[0] && ( + <Modal + containerClassName={styles.modal} + open={isOpenPaymentModal} + onClose={handleCloseModal} + > + <PaymentModal + placementKey={EPlacementKeys["aura.placement.email.marketing"]} + /> + </Modal> + )} <section className={`${styles.page} page`}> <div className={styles.wrapper}> <div className={styles.banner}>Special Offer</div> @@ -55,7 +52,7 @@ function MarketingTrialPayment() { <p className={styles.description}>No pressure. Cancel anytime</p> <div className={styles["total-today"]}> <p className={styles.description}>Total today:</p> - <p className={styles.value}>$0</p> + <p className={styles.value}>${(products[0]?.trialPrice / 100).toFixed(2) || 0}</p> </div> <div className={styles.line} /> <div className={styles["code-container"]}> @@ -74,7 +71,7 @@ function MarketingTrialPayment() { <p className={styles["sale-description"]}>Save $10 every period</p> <div className={styles.line} /> <p className={styles["text-description"]}> - You will be charged only <b>$0 for your 7-day trial.</b>{" "} + You will be charged only <b>${(products[0]?.trialPrice / 100).toFixed(2) || 0} for your 7-day trial.</b>{" "} Subscription <b>renews automatically</b> until cancelled. You{" "} <b>can cancel at any time</b> before the end of the trial. </p> diff --git a/src/components/pages/Gender/index.tsx b/src/components/pages/Gender/index.tsx index 725871f..199a643 100644 --- a/src/components/pages/Gender/index.tsx +++ b/src/components/pages/Gender/index.tsx @@ -20,6 +20,11 @@ function GenderPage({ productKey }: IGenderPageProps): JSX.Element { useEffect(() => { const isShowTryApp = targetId === "i"; dispatch(actions.userConfig.addIsShowTryApp(isShowTryApp)); + if (targetId && typeof window.ym === "function" && targetId !== "i") { + window.ym(95799066, "userParams", { + genderFrom: targetId, + }); + } }, [dispatch, targetId]); const selectGender = (gender: Gender) => { diff --git a/src/components/pages/GetInformationPartner/index.tsx b/src/components/pages/GetInformationPartner/index.tsx index 53e8454..effc206 100644 --- a/src/components/pages/GetInformationPartner/index.tsx +++ b/src/components/pages/GetInformationPartner/index.tsx @@ -17,7 +17,13 @@ function GetInformationPartnerPage() { return ( <section className={`${styles.page} page`}> - <video className={styles["background-video"]} loop autoPlay muted> + <video + className={styles["background-video"]} + loop={true} + autoPlay={true} + muted={true} + playsInline={true} + > <source src="/videos/background-video-1.mp4" type="video/mp4" /> <source src="/videos/background-video-1.mp4" type="video/ogg" /> Your browser does not support the video tag. diff --git a/src/components/pages/Onboarding/index.tsx b/src/components/pages/Onboarding/index.tsx index 6d0fbb0..8549e8a 100755 --- a/src/components/pages/Onboarding/index.tsx +++ b/src/components/pages/Onboarding/index.tsx @@ -4,6 +4,8 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { onboardingTitles } from "@/data/onboarding"; import routes from "@/routes"; import { useNavigate } from "react-router-dom"; +import { usePaywall } from "@/hooks/paywall/usePaywall"; +import { EPlacementKeys } from "@/api/resources/Paywall"; function OnboardingPage() { const navigate = useNavigate(); @@ -11,6 +13,7 @@ function OnboardingPage() { const [periodClassName, setPeriodClassName] = useState(""); const titleInterval = useRef<NodeJS.Timeout>(); const classNameTimeOut = useRef<NodeJS.Timeout>(); + usePaywall({ placementKey: EPlacementKeys["aura.placement.main"] }); const handleNext = useCallback(() => { navigate(routes.client.trialChoice()); diff --git a/src/components/pages/PaymentWithEmailPage/index.tsx b/src/components/pages/PaymentWithEmailPage/index.tsx deleted file mode 100644 index 4e4bdb3..0000000 --- a/src/components/pages/PaymentWithEmailPage/index.tsx +++ /dev/null @@ -1,248 +0,0 @@ -import EmailInput from "@/components/EmailEnterPage/EmailInput"; -import styles from "./styles.module.css"; -import { useCallback, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { actions, selectors } from "@/store"; -import { useDispatch, useSelector } from "react-redux"; -import MainButton from "@/components/MainButton"; -import Loader, { LoaderColor } from "@/components/Loader"; -import { useAuth } from "@/auth"; -import { ApiError, extractErrorMessage, useApi } from "@/api"; -import { getClientTimezone } from "@/locales"; -import ErrorText from "@/components/ErrorText"; -import Title from "@/components/Title"; -import NameInput from "@/components/EmailEnterPage/NameInput"; -import { useParams } from "react-router-dom"; -import routes from "@/routes"; -import PaymentForm from "./PaymentForm"; -import { getPriceCentsToDollars } from "@/services/price"; -import { useSinglePayment } from "@/hooks/payment/useSinglePayment"; - -function PaymentWithEmailPage() { - const { productId } = useParams(); - const { t, i18n } = useTranslation(); - // const tokenFromStore = useSelector(selectors.selectToken); - const { signUp, user: userFromStore, token: tokenFromStore } = useAuth(); - const api = useApi(); - const timezone = getClientTimezone(); - const dispatch = useDispatch(); - const birthday = useSelector(selectors.selectBirthday); - const locale = i18n.language; - const [email, setEmail] = useState(""); - const [name, setName] = useState(""); - const [isValidEmail, setIsValidEmail] = useState(false); - const [isValidName, setIsValidName] = useState(productId !== "chat.aura"); - const [isDisabled, setIsDisabled] = useState(true); - const [isAuth, setIsAuth] = useState(false); - const [apiError, setApiError] = useState<ApiError | null>(null); - const [error, setError] = useState<boolean>(false); - const returnUrl = `${window.location.protocol}//${ - window.location.host - }${routes.client.paymentResult()}`; - - const [isLoadingAuth, setIsLoadingAuth] = useState<boolean>(false); - - const { - product, - paymentIntent, - createSinglePayment, - isLoading: isLoadingSinglePayment, - error: errorSinglePayment, - } = useSinglePayment(); - - useEffect(() => { - if ( - isValidName && - isValidEmail && - !(error || apiError || errorSinglePayment?.error) - ) { - setIsDisabled(false); - } else { - setIsDisabled(true); - } - }, [ - isValidEmail, - email, - isValidName, - name, - error, - apiError, - errorSinglePayment?.error, - ]); - - const handleValidEmail = (email: string) => { - dispatch(actions.form.addEmail(email)); - setEmail(email); - setIsValidEmail(true); - }; - - const handleValidName = (name: string) => { - setName(name); - setIsValidName(true); - }; - - const authorization = async () => { - try { - setIsLoadingAuth(true); - const auth = await api.auth({ email, timezone, locale }); - const { - auth: { token, user }, - } = auth; - signUp(token, user); - const payload = { - user: { - profile_attributes: { - birthday, - full_name: name, - }, - }, - 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")); - setIsAuth(true); - const userUpdated = await api.getUser({ token }); - setIsLoadingAuth(false); - return { user: userUpdated?.user, token }; - } catch (error) { - console.error(error); - if (error instanceof ApiError) { - setApiError(error as ApiError); - } else { - setError(true); - } - setIsLoadingAuth(false); - } - }; - - const handleClick = async () => { - const authData = await authorization(); - if (!authData) { - return; - } - const { user, token } = authData; - if (typeof window.ym === "function") - window.ym(95799066, "reachGoal", "EnteredEmail"); - - await createSinglePayment({ - user, - token, - targetProductKey: productId || "", - returnUrl, - }); - }; - - const handleAuthUser = useCallback(async () => { - if (!tokenFromStore.length || !userFromStore) { - return; - } - - await createSinglePayment({ - user: userFromStore, - token: tokenFromStore, - targetProductKey: productId || "", - returnUrl, - }); - }, [ - createSinglePayment, - productId, - returnUrl, - tokenFromStore, - userFromStore, - ]); - - useEffect(() => { - handleAuthUser(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - <div className={`${styles.page} page`}> - {(isLoadingSinglePayment || isLoadingSinglePayment) && ( - <Loader color={LoaderColor.Black} /> - )} - {!isLoadingSinglePayment && - !isLoadingAuth && - paymentIntent && - "paymentIntent" in paymentIntent && - !!tokenFromStore.length && ( - <> - <Title variant="h1" className={styles.title}> - {getPriceCentsToDollars(product?.amount || 0)}$ - - - - )} - {(!tokenFromStore || !paymentIntent) && - // || (productId !== "chat.aura" && !name.length) - !isLoadingSinglePayment && - !isLoadingAuth && ( - <> - setIsValidName(productId !== "chat.aura")} - /> - setIsValidEmail(false)} - /> - - - {isLoadingSinglePayment && } - {!isLoadingSinglePayment && - !(!apiError && !error && !isLoadingSinglePayment && isAuth) && - t("_continue")} - {!apiError && !error && !isLoadingSinglePayment && isAuth && ( - Success Icon - )} - - - )} - {(error || apiError || errorSinglePayment?.error) && ( - - Something went wrong:{" "} - {errorSinglePayment?.error?.length && errorSinglePayment?.error} - - )} - {apiError && ( - - )} -
- ); -} - -export default PaymentWithEmailPage; diff --git a/src/components/pages/PaymentWithEmailPage/styles.module.css b/src/components/pages/PaymentWithEmailPage/styles.module.css deleted file mode 100644 index 27971fd..0000000 --- a/src/components/pages/PaymentWithEmailPage/styles.module.css +++ /dev/null @@ -1,61 +0,0 @@ -.page { - /* position: relative; */ - position: static; - height: fit-content; - min-height: calc(100dvh - 103px); - /* max-height: -webkit-fill-available; */ - display: flex; - justify-items: center; - justify-content: center; - align-items: center; - /* gap: 16px; */ -} - -.button { - border-radius: 12px; - margin-top: 0; - box-shadow: rgba(0, 0, 0, 0.25) 0px 4px 4px 0px; - height: 50px; - min-height: 0; - background: linear-gradient( - 165.54deg, - rgb(20, 19, 51) -33.39%, - rgb(32, 34, 97) 15.89%, - rgb(84, 60, 151) 55.84%, - rgb(105, 57, 162) 74.96% - ); - font-size: 18px; - line-height: 21px; -} - -.payment-loader { - display: flex; - justify-content: center; - align-items: center; -} - -.cross { - position: absolute; - top: -36px; - right: 28px; - width: 22px; - height: 22px; - cursor: pointer; - z-index: 9; -} - -.title { - font-size: 27px; - font-weight: 700; - margin: 0; -} - -.email { - font-size: 17px; - font-weight: 500; - margin: 0; -} - -.success-icon { - height: 100%; -} diff --git a/src/components/pages/PaymentWithEmailPage/PaymentForm/index.tsx b/src/components/pages/SinglePaymentPage/PaymentForm/index.tsx similarity index 78% rename from src/components/pages/PaymentWithEmailPage/PaymentForm/index.tsx rename to src/components/pages/SinglePaymentPage/PaymentForm/index.tsx index bb6a583..ecaf0d1 100644 --- a/src/components/pages/PaymentWithEmailPage/PaymentForm/index.tsx +++ b/src/components/pages/SinglePaymentPage/PaymentForm/index.tsx @@ -1,6 +1,6 @@ import { Elements } from "@stripe/react-stripe-js"; import styles from "./styles.module.css"; -import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm"; +import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm"; import { useEffect, useState } from "react"; import { Stripe, loadStripe } from "@stripe/stripe-js"; import SecurityPayments from "../../TrialPayment/components/SecurityPayments"; @@ -9,9 +9,15 @@ interface IPaymentFormProps { stripePublicKey: string; clientSecret: string; returnUrl: string; + confirmType?: "payment" | "setup"; } -function PaymentForm({ stripePublicKey, clientSecret, returnUrl }: IPaymentFormProps) { +function PaymentForm({ + stripePublicKey, + clientSecret, + returnUrl, + confirmType = "payment", +}: IPaymentFormProps) { const [stripePromise, setStripePromise] = useState | null>(null); @@ -23,7 +29,7 @@ function PaymentForm({ stripePublicKey, clientSecret, returnUrl }: IPaymentFormP
{stripePromise && clientSecret && ( - + )}
diff --git a/src/components/pages/PaymentWithEmailPage/PaymentForm/styles.module.css b/src/components/pages/SinglePaymentPage/PaymentForm/styles.module.css similarity index 100% rename from src/components/pages/PaymentWithEmailPage/PaymentForm/styles.module.css rename to src/components/pages/SinglePaymentPage/PaymentForm/styles.module.css diff --git a/src/components/pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage/index.tsx b/src/components/pages/SinglePaymentPage/ResultPayment/FailPaymentPage/index.tsx similarity index 100% rename from src/components/pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage/index.tsx rename to src/components/pages/SinglePaymentPage/ResultPayment/FailPaymentPage/index.tsx diff --git a/src/components/pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage/styles.module.css b/src/components/pages/SinglePaymentPage/ResultPayment/FailPaymentPage/styles.module.css similarity index 100% rename from src/components/pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage/styles.module.css rename to src/components/pages/SinglePaymentPage/ResultPayment/FailPaymentPage/styles.module.css diff --git a/src/components/pages/PaymentWithEmailPage/ResultPayment/SuccessPaymentPage/index.tsx b/src/components/pages/SinglePaymentPage/ResultPayment/SuccessPaymentPage/index.tsx similarity index 100% rename from src/components/pages/PaymentWithEmailPage/ResultPayment/SuccessPaymentPage/index.tsx rename to src/components/pages/SinglePaymentPage/ResultPayment/SuccessPaymentPage/index.tsx diff --git a/src/components/pages/PaymentWithEmailPage/ResultPayment/SuccessPaymentPage/styles.module.css b/src/components/pages/SinglePaymentPage/ResultPayment/SuccessPaymentPage/styles.module.css similarity index 100% rename from src/components/pages/PaymentWithEmailPage/ResultPayment/SuccessPaymentPage/styles.module.css rename to src/components/pages/SinglePaymentPage/ResultPayment/SuccessPaymentPage/styles.module.css diff --git a/src/components/pages/SinglePaymentPage/index.tsx b/src/components/pages/SinglePaymentPage/index.tsx index 5081c52..b662985 100644 --- a/src/components/pages/SinglePaymentPage/index.tsx +++ b/src/components/pages/SinglePaymentPage/index.tsx @@ -1,6 +1,6 @@ import Title from "@/components/Title"; import styles from "./styles.module.css"; -import PaymentForm from "../PaymentWithEmailPage/PaymentForm"; +import PaymentForm from "./PaymentForm"; import { getPriceCentsToDollars } from "@/services/price"; import { useSinglePayment } from "@/hooks/payment/useSinglePayment"; import routes from "@/routes"; @@ -59,7 +59,7 @@ function SinglePaymentPage({ productId, isForce = false }: ISinglePaymentPage) { !!tokenFromStore.length && ( <> - {getPriceCentsToDollars(product?.amount || 0)}$ + {getPriceCentsToDollars(product?.price || 0)}$ ([]); const [isDisabled, setIsDisabled] = useState(true); - const allowedPlans = useMemo(() => [""], []); + const [countUsers, setCountUsers] = useState(752); useEffect(() => { - (async () => { - const { sub_plans } = await api.getSubscriptionPlans({ locale }); - const plansWithoutTest = sub_plans.filter( - (plan: ISubscriptionPlan) => !plan.name.includes("(test)") - ); - const plansKeys: IPlanKey = {}; - const plans: ISubscriptionPlan[] = []; - for (const plan of plansWithoutTest) { - plansKeys[plan.name] = plansKeys[plan.name] - ? plansKeys[plan.name] + 1 - : 1; - if ( - (plansKeys[plan.name] > 1 && !plan.trial?.is_free && !!plan.trial) || - allowedPlans.includes(plan.id) - ) { - const targetPlan = plansWithoutTest.find( - (item) => item.name === plan.name && item.id.includes("stripe") - ); - plans.push(targetPlan as ISubscriptionPlan); - } - } + const randomDelay = getRandomArbitrary(3000, 5000); + const countUsersTimeOut = setTimeout(() => { + setCountUsers((prevState) => prevState + 1); + }, randomDelay); + return () => clearTimeout(countUsersTimeOut); + }, [countUsers]); - plans.sort((a, b) => { - if (!a.trial || !b.trial) { - return 0; - } - if (a.trial?.price_cents < b.trial?.price_cents) { - return -1; - } - if (a.trial?.price_cents > b.trial?.price_cents) { - return 1; - } - return 0; - }); - - setSubPlans(plans); - })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [api, locale]); + const { products, getText } = usePaywall({ + placementKey: EPlacementKeys["aura.placement.main"], + }); const handlePriceItem = () => { setIsDisabled(false); @@ -85,38 +48,49 @@ function TrialChoicePage() { return (

- We've helped millions of people to have happier lives and better - relationships, and we want to help you too. + {getText("text.0", { + replacementSelector: "b", + color: "#1C38EA", + })}

- Money shouldn’t stand in the way of finding astrology guidance that - finally works. So, choose an amount that you think is reasonable to try - us out for one week. + {getText("text.1", { + color: "#1C38EA", + })}

- It costs us $13.67 to offer a 3-day trial, but please choose the amount - you are comfortable with. + {getText("text.2", { + color: "#1C38EA", + })}

- This option will help us support those who need to select the lowest - trial prices! + {getText("text.3", { + color: "#1C38EA", + })}

{`Arrow
- See my plan + {getText("text.button.1", { + color: "#1C38EA", + })}

- *Cost of trial as of February 2024 + {getText("text.4", { + color: "#1C38EA", + })}

); diff --git a/src/components/pages/TrialPayment/components/PaymentModal/index.tsx b/src/components/pages/TrialPayment/components/PaymentModal/index.tsx index 660ecc4..22f4a66 100644 --- a/src/components/pages/TrialPayment/components/PaymentModal/index.tsx +++ b/src/components/pages/TrialPayment/components/PaymentModal/index.tsx @@ -4,48 +4,62 @@ import PaymentMethodsChoice from "../PaymentMethodsChoice"; import { useEffect, useMemo, useState } from "react"; import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods"; import { Elements } from "@stripe/react-stripe-js"; -import ApplePayButton from "@/components/StripePage/ApplePayButton"; -import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm"; +import ApplePayButton from "@/components/PaymentPage/methods/ApplePayButton"; +import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm"; import { Stripe, loadStripe } from "@stripe/stripe-js"; import { useSelector } from "react-redux"; import { selectors } from "@/store"; -import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans"; -import { useApi } from "@/api"; -import { useNavigate } from "react-router-dom"; -import routes from "@/routes"; -import { useTranslation } from "react-i18next"; -import { useAuth } from "@/auth"; import Loader from "@/components/Loader"; -import { getPriceFromTrial } from "@/services/price"; import SecurityPayments from "../SecurityPayments"; +import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall"; +import { usePaywall } from "@/hooks/paywall/usePaywall"; +import { useMakePayment } from "@/hooks/payment/useMakePayment"; interface IPaymentModalProps { - activeSubscriptionPlan?: ISubscriptionPlan; + activeProduct?: IPaywallProduct; noTrial?: boolean; returnUrl?: string; + placementKey?: EPlacementKeys; } +const getPrice = (product: IPaywallProduct | null) => { + if (!product) { + return 0; + } + return (product.trialPrice === 100 ? 99 : product.trialPrice || 0) / 100; +}; + function PaymentModal({ - activeSubscriptionPlan, + activeProduct, noTrial, returnUrl, + placementKey = EPlacementKeys["aura.placement.main"], }: IPaymentModalProps) { - const { i18n } = useTranslation(); - const locale = i18n.language; - const api = useApi(); - const { token } = useAuth(); - const navigate = useNavigate(); - const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan); - const activeSubPlan = activeSubscriptionPlan - ? activeSubscriptionPlan - : activeSubPlanFromStore; const [stripePromise, setStripePromise] = useState | null>(null); - const [clientSecret, setClientSecret] = useState(""); - const [subscriptionReceiptId, setSubscriptionReceiptId] = - useState(""); - const [isLoading, setIsLoading] = useState(true); - const [isError, setIsError] = useState(false); + + const { products } = usePaywall({ + placementKey, + }); + const activeProductFromStore = useSelector(selectors.selectActiveProduct); + + const _activeProduct = activeProduct ? activeProduct : activeProductFromStore; + const { + paymentIntentId, + clientSecret, + returnUrl: checkoutUrl, + paymentType, + publicKey, + isLoading, + error, + } = useMakePayment({ + productId: _activeProduct?._id || "", + returnPaidUrl: returnUrl, + }); + + if (checkoutUrl?.length) { + window.location.href = checkoutUrl; + } const paymentMethodsButtons = useMemo(() => { // return paymentMethods.filter( @@ -64,43 +78,10 @@ function PaymentModal({ useEffect(() => { (async () => { - const siteConfig = await api.getAppConfig({ bundleId: "auraweb" }); - setStripePromise(loadStripe(siteConfig.data.stripe_public_key)); - const { sub_plans } = await api.getSubscriptionPlans({ locale }); - const isActiveSubPlan = sub_plans.find( - (subPlan) => subPlan.id === activeSubPlan?.id - ); - if (!activeSubPlan || !isActiveSubPlan) { - navigate(routes.client.priceList()); - } + if (!products?.length || !publicKey) return; + setStripePromise(loadStripe(publicKey)); })(); - }, [activeSubPlan, api, locale, navigate]); - - useEffect(() => { - (async () => { - try { - const { subscription_receipt } = await api.createSubscriptionReceipt({ - token, - way: "stripe", - subscription_receipt: { - sub_plan_id: activeSubPlan?.id || "stripe.7", - }, - }); - const { id } = subscription_receipt; - const { client_secret } = subscription_receipt.data; - const { checkout_url } = subscription_receipt.data; - if (checkout_url?.length) { - window.location.href = checkout_url; - } - setSubscriptionReceiptId(id); - setClientSecret(client_secret); - setIsLoading(false); - } catch (error) { - console.error(error); - setIsError(true); - } - })(); - }, [activeSubPlan?.id, api, token]); + }, [products, publicKey]); if (isLoading) { return ( @@ -112,7 +93,7 @@ function PaymentModal({ ); } - if (isError) { + if (error?.length) { return (
@@ -132,16 +113,13 @@ function PaymentModal({ selectedPaymentMethod={selectedPaymentMethod} onSelectPaymentMethod={onSelectPaymentMethod} /> - {activeSubPlan && ( + {_activeProduct && ( <div> {!noTrial && ( <> <p className={styles["sub-plan-description"]}> You will be charged only{" "} - <b> - ${getPriceFromTrial(activeSubPlan?.trial)} for your 3-day - trial. - </b> + <b>${getPrice(_activeProduct)} for your 3-day trial.</b> </p> <p className={styles["sub-plan-description"]}> We`ll <b>email you a reminder</b> before your trial period ends. @@ -160,16 +138,17 @@ function PaymentModal({ {selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && ( <div className={styles["payment-method"]}> <ApplePayButton - activeSubPlan={activeSubPlan} + activeProduct={_activeProduct} client_secret={clientSecret} - subscriptionReceiptId={subscriptionReceiptId} + subscriptionReceiptId={paymentIntentId} /> </div> )} {selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && ( <CheckoutForm - subscriptionReceiptId={subscriptionReceiptId} + confirmType={paymentType} + subscriptionReceiptId={paymentIntentId} returnUrl={returnUrl} /> )} diff --git a/src/components/pages/TrialPayment/components/PaymentTable/index.tsx b/src/components/pages/TrialPayment/components/PaymentTable/index.tsx index 9551357..b193a65 100755 --- a/src/components/pages/TrialPayment/components/PaymentTable/index.tsx +++ b/src/components/pages/TrialPayment/components/PaymentTable/index.tsx @@ -1,18 +1,21 @@ import Title from "@/components/Title"; import styles from "./styles.module.css"; -import { getPriceFromTrial } from "@/services/price"; -import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans"; import CustomButton from "../CustomButton"; import GuardPayments from "../GuardPayments"; import { useState } from "react"; import FullScreenModal from "@/components/FullScreenModal"; +import { IPaywallProduct } from "@/api/resources/Paywall"; interface IPaymentTableProps { - subPlan: ISubscriptionPlan; + product: IPaywallProduct; buttonClick: () => void; } -function PaymentTable({ subPlan, buttonClick }: IPaymentTableProps) { +const getPrice = (product: IPaywallProduct) => { + return (product.trialPrice || 0) / 100; +}; + +function PaymentTable({ product, buttonClick }: IPaymentTableProps) { const [isOpenPrivacyModal, setIsOpenPrivacyModal] = useState<boolean>(false); const handleSubscriptionPolicyClick = (event: React.MouseEvent) => { event.preventDefault(); @@ -44,20 +47,18 @@ function PaymentTable({ subPlan, buttonClick }: IPaymentTableProps) { <div className={styles["table-container"]}> <Title variant="h3" className={styles.title}> Personalized reading for{" "} - <span className={styles.purple}> - ${getPriceFromTrial(subPlan?.trial)} - </span> + <span className={styles.purple}>${getPrice(product)}</span>

Total today:

- ${getPriceFromTrial(subPlan?.trial)} + ${getPrice(product)}

Your cost per 2 weeks after trial

$65 - ${subPlan.price_cents / 100} + ${product.trialPrice / 100}
@@ -69,9 +70,9 @@ function PaymentTable({ subPlan, buttonClick }: IPaymentTableProps) {

You are enrolling in 2 weeks subscription. By continuing you agree that if you don't cancel prior to the end of the 3-day trial for the $ - {getPriceFromTrial(subPlan?.trial)} you will automatically be charged - $19 every 2 weeks until you cancel in settings. Learn more about - cancellation and refund policy in{" "} + {getPrice(product)} you will automatically be charged $19 every 2 weeks + until you cancel in settings. Learn more about cancellation and refund + policy in{" "} Subscription policy

diff --git a/src/components/pages/TrialPayment/index.tsx b/src/components/pages/TrialPayment/index.tsx index 94513fd..d783f1a 100755 --- a/src/components/pages/TrialPayment/index.tsx +++ b/src/components/pages/TrialPayment/index.tsx @@ -13,22 +13,17 @@ import YourReading from "./components/YourReading"; import Reviews from "./components/Reviews"; import PointsList from "./components/PointsList"; import OftenAsk from "./components/OftenAsk"; -import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans"; import { useEffect, useState } from "react"; -import { useApi } from "@/api"; -import { getClientLocale } from "@/locales"; -import { Locale } from "@/components/PaymentTable"; import WithPartnerInformation from "./components/WithPartnerInformation"; import Modal from "@/components/Modal"; import PaymentModal from "./components/PaymentModal"; import { trialPaymentPointsList } from "@/data/pointsLists"; import { trialPaymentReviews } from "@/data/reviews"; - -const locale = getClientLocale() as Locale; +import { usePaywall } from "@/hooks/paywall/usePaywall"; +import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall"; function TrialPaymentPage() { const dispatch = useDispatch(); - const api = useApi(); const navigate = useNavigate(); const birthdate = useSelector(selectors.selectBirthdate); const zodiacSign = getZodiacSignByDate(birthdate); @@ -42,55 +37,46 @@ function TrialPaymentPage() { flowChoice, } = useSelector(selectors.selectQuestionnaire); const partnerZodiacSign = getZodiacSignByDate(partnerBirthdate); - const [subPlans, setSubPlans] = useState([]); - const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan); - const [activeSubPlan, setActiveSubPlan] = useState( - activeSubPlanFromStore - ); const [isOpenPaymentModal, setIsOpenPaymentModal] = useState(false); const [marginTopTitle, setMarginTopTitle] = useState(360); const [singleOrWithPartner, setSingleOrWithPartner] = useState< "single" | "partner" >("single"); const { subPlan } = useParams(); - - useEffect(() => { - (async () => { - const { sub_plans } = await api.getSubscriptionPlans({ locale }); - const plans = sub_plans - .filter((plan: ISubscriptionPlan) => plan.provider === "stripe") - .sort((a, b) => { - if (!a.trial || !b.trial) { - return 0; - } - if (a?.trial?.price_cents < b?.trial?.price_cents) { - return -1; - } - if (a?.trial?.price_cents > b?.trial?.price_cents) { - return 1; - } - return 0; - }); - setSubPlans(plans); - })(); - }, [api]); + const { products } = usePaywall({ + placementKey: EPlacementKeys["aura.placement.main"], + }); + const activeProductFromStore = useSelector(selectors.selectActiveProduct); + const [activeProduct, setActiveProduct] = useState( + activeProductFromStore + ); useEffect(() => { if (subPlan) { - const targetSubPlan = subPlans.find( - (sub_plan) => + const targetProduct = products.find( + (product) => String( - sub_plan?.trial?.price_cents - ? Math.floor((sub_plan?.trial?.price_cents + 1) / 100) - : sub_plan.id.replace(".", "") + product?.trialPrice + ? Math.floor((product?.trialPrice + 1) / 100) + : product.key.replace(".", "") ) === subPlan ); - if (targetSubPlan) { - setActiveSubPlan(targetSubPlan); - dispatch(actions.payment.update({ activeSubPlan: targetSubPlan })); + if (targetProduct) { + setActiveProduct(targetProduct); + dispatch(actions.payment.update({ activeProduct })); } } - }, [dispatch, subPlan, subPlans]); + }, [dispatch, subPlan, products, activeProduct]); + + useEffect(() => { + if (!products.length) return; + const isActiveProduct = products.find( + (product) => product._id === activeProduct?._id + ); + if (!activeProduct || !isActiveProduct) { + navigate(routes.client.trialChoice()); + } + }, [activeProduct, navigate, products]); useEffect(() => { if (["relationship", "married"].includes(flowChoice)) { @@ -102,7 +88,7 @@ function TrialPaymentPage() { setMarginTopTitle(340); }, [flowChoice]); - if (!activeSubPlan) { + if (!activeProduct) { return ; } @@ -157,7 +143,7 @@ function TrialPaymentPage() { Your Personalized Clarity & Love Reading is ready! - + - +
); } diff --git a/src/components/pages/TrialPaymentWithDiscount/PaymentDiscountTable/index.tsx b/src/components/pages/TrialPaymentWithDiscount/PaymentDiscountTable/index.tsx index a18d5ee..3157556 100644 --- a/src/components/pages/TrialPaymentWithDiscount/PaymentDiscountTable/index.tsx +++ b/src/components/pages/TrialPaymentWithDiscount/PaymentDiscountTable/index.tsx @@ -2,10 +2,17 @@ import Title from "@/components/Title"; import styles from "./styles.module.css"; import { useSelector } from "react-redux"; import { selectors } from "@/store"; -import { getPriceFromTrial } from "@/services/price"; +import { IPaywallProduct } from "@/api/resources/Paywall"; + +const getPrice = (product: IPaywallProduct | null) => { + if (!product) { + return 0; + } + return (product.trialPrice === 100 ? 99 : product.trialPrice || 0) / 100; +}; function PaymentDiscountTable() { - const activeSub = useSelector(selectors.selectActiveSubPlan); + const activeProduct = useSelector(selectors.selectActiveProduct); return (
@@ -15,7 +22,11 @@ function PaymentDiscountTable() {

No pressure. Cancel anytime.

- Present + Present

Secret discount applied!

@@ -27,14 +38,14 @@ function PaymentDiscountTable() {

Your cost per 14 days after trial:

$19 - $9 + ${(activeProduct?.price || 0) / 100}

You save $30


Total today:

- {activeSub && ${getPriceFromTrial(activeSub.trial)}} + {activeProduct && ${getPrice(activeProduct)}}
); diff --git a/src/components/pages/TrialPaymentWithDiscount/index.tsx b/src/components/pages/TrialPaymentWithDiscount/index.tsx index 4122374..2eb3226 100644 --- a/src/components/pages/TrialPaymentWithDiscount/index.tsx +++ b/src/components/pages/TrialPaymentWithDiscount/index.tsx @@ -4,11 +4,35 @@ import MainButton from "@/components/MainButton"; import PaymentDiscountTable from "./PaymentDiscountTable"; import Modal from "@/components/Modal"; import PaymentModal from "../TrialPayment/components/PaymentModal"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { usePaywall } from "@/hooks/paywall/usePaywall"; +import { EPlacementKeys } from "@/api/resources/Paywall"; +import { useDispatch, useSelector } from "react-redux"; +import { actions, selectors } from "@/store"; function TrialPaymentWithDiscount() { + const dispatch = useDispatch(); + const { products } = usePaywall({ + placementKey: EPlacementKeys["aura.placement.secret.discount"], + }); + const productFromStore = useSelector(selectors.selectActiveProduct); + const [isOpenPaymentModal, setIsOpenPaymentModal] = useState(false); + useEffect(() => { + if (!products.length) return; + const activeProduct = products.find( + (p) => p.trialPrice === productFromStore?.trialPrice + ); + if (!activeProduct) { + dispatch(actions.payment.update({ activeProduct: products[0] })); + } + if (activeProduct) { + dispatch(actions.payment.update({ activeProduct })); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dispatch, products]); + const handleClose = () => { setIsOpenPaymentModal(false); }; @@ -16,7 +40,9 @@ function TrialPaymentWithDiscount() { return (
- +

- By continuing you agree that if you don't cancel prior to the end of the - 3-days trial, you will automatically be charged $9 for the introductory - period of 14 days thereafter the standard rate of $9 every 14 days until - you cancel in settings. Learn more about cancellation and refund policy - in Subscription terms. + By continuing you agree that if you don`t cancel prior to the end of the + 3-days trial, you will automatically be charged $ + {(productFromStore?.price || 0) / 100} for the introductory period of 14 + days thereafter the standard rate of $ + {(productFromStore?.price || 0) / 100} every 14 days until you cancel in + settings. Learn more about cancellation and refund policy in + Subscription terms.

); diff --git a/src/components/palmistry/discount-screen/discount-screen.tsx b/src/components/palmistry/discount-screen/discount-screen.tsx index a30f176..29dcca5 100644 --- a/src/components/palmistry/discount-screen/discount-screen.tsx +++ b/src/components/palmistry/discount-screen/discount-screen.tsx @@ -11,7 +11,7 @@ import routes from "@/routes"; import { useApi } from "@/api"; import { useAuth } from "@/auth"; import HeaderLogo from "@/components/palmistry/header-logo/header-logo"; -import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm"; +import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm"; import { ResponseGet } from "@/api/resources/SinglePayment"; const currentProductKey = "skip.trial.subscription.aura"; @@ -72,9 +72,9 @@ export default function DiscountScreen() { const buy = async () => { if (!user?.id || !product) return; - const { productId, key } = product; + const { _id, key } = product; const paymentInfo = { - productId, + productId: _id, key, }; diff --git a/src/components/palmistry/payment-screen/payment-screen.tsx b/src/components/palmistry/payment-screen/payment-screen.tsx index 2895207..23e89ee 100644 --- a/src/components/palmistry/payment-screen/payment-screen.tsx +++ b/src/components/palmistry/payment-screen/payment-screen.tsx @@ -2,22 +2,29 @@ import React from "react"; import { useSelector } from "react-redux"; -import './payment-screen.css'; +import "./payment-screen.css"; -import useSteps, { Step } from '@/hooks/palmistry/use-steps'; -import useTimer from '@/hooks/palmistry/use-timer'; -import HeaderLogo from '@/components/palmistry/header-logo/header-logo'; +import useSteps, { Step } from "@/hooks/palmistry/use-steps"; +import useTimer from "@/hooks/palmistry/use-timer"; +import HeaderLogo from "@/components/palmistry/header-logo/header-logo"; import PaymentModal from "@/components/pages/TrialPayment/components/PaymentModal"; import { selectors } from "@/store"; +import { EPlacementKeys } from "@/api/resources/Paywall"; +import { useSearchParams } from "react-router-dom"; const getFormattedPrice = (price: number) => { return (price / 100).toFixed(2); -} +}; export default function PaymentScreen() { const time = useTimer(); - const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan); - const subscriptionStatus = useSelector(selectors.selectStatus); + const activeProductFromStore = useSelector(selectors.selectActiveProduct); + const [searchParams] = useSearchParams(); + const subscriptionStatus = + searchParams.get("redirect_status") === "succeeded" + ? "subscribed" + : "lead"; + // const subscriptionStatus = useSelector(selectors.selectStatus); const steps = useSteps(); @@ -27,18 +34,20 @@ export default function PaymentScreen() { steps.goNext(); }, 1500); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [subscriptionStatus]); React.useEffect(() => { - if (!activeSubPlanFromStore) { + if (!activeProductFromStore) { steps.setFirstUnpassedStep(Step.SubscriptionPlan); } - }, [activeSubPlanFromStore]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeProductFromStore]); - const trialPrice = activeSubPlanFromStore?.trial?.price_cents || 0; - const fullPrice = activeSubPlanFromStore?.price_cents || 0; + const trialPrice = activeProductFromStore?.trialPrice || 0; + const fullPrice = activeProductFromStore?.price || 0; - const [minutes, seconds] = time.split(':'); + const [minutes, seconds] = time.split(":"); return (
@@ -236,9 +245,20 @@ export default function PaymentScreen() { - {activeSubPlanFromStore && ( -
- {subscriptionStatus !== "subscribed" && } + {activeProductFromStore && ( +
+ {subscriptionStatus !== "subscribed" && ( + + )} {subscriptionStatus === "subscribed" && (
@@ -255,7 +275,9 @@ export default function PaymentScreen() { /> -
Payment success
+
+ Payment success +
)}
diff --git a/src/components/palmistry/premium-bundle-screen/premium-bundle-screen.tsx b/src/components/palmistry/premium-bundle-screen/premium-bundle-screen.tsx index 0d34d81..60da2dd 100644 --- a/src/components/palmistry/premium-bundle-screen/premium-bundle-screen.tsx +++ b/src/components/palmistry/premium-bundle-screen/premium-bundle-screen.tsx @@ -10,7 +10,7 @@ import routes from "@/routes"; import HeaderLogo from "@/components/palmistry/header-logo/header-logo"; import { useApi } from "@/api"; import { useAuth } from "@/auth"; -import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm"; +import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm"; import { ResponseGet } from "@/api/resources/SinglePayment"; const currentProductKey = "premium.bundle.aura"; @@ -52,7 +52,7 @@ export default function PremiumBundleScreen() { const buy = async () => { if (!user?.id || !product) return; - const { productId, key } = product; + const { _id, key } = product; const response = await api.createSinglePayment({ token: token, @@ -69,7 +69,7 @@ export default function PremiumBundleScreen() { age: 0, }, paymentInfo: { - productId, + productId: _id, key, }, return_url: returnUrl, diff --git a/src/components/palmistry/step-subscription-plan/step-subscription-plan.tsx b/src/components/palmistry/step-subscription-plan/step-subscription-plan.tsx index 6b44a35..3620bc7 100644 --- a/src/components/palmistry/step-subscription-plan/step-subscription-plan.tsx +++ b/src/components/palmistry/step-subscription-plan/step-subscription-plan.tsx @@ -1,92 +1,51 @@ -import React, { useMemo } from "react"; +import React from "react"; import { useDispatch } from "react-redux"; -import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { selectors } from "@/store"; import useSteps, { Step } from "@/hooks/palmistry/use-steps"; import Button from "@/components/palmistry/button/button"; import EmailHeader from "@/components/palmistry/email-header/email-header"; -import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans"; import { actions } from "@/store"; -import { useApi } from "@/api"; +import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall"; +import { usePaywall } from "@/hooks/paywall/usePaywall"; const bestPlanId = "stripe.15"; -const getFormattedPrice = (plan: ISubscriptionPlan) => { - return (plan.trial!.price_cents / 100).toFixed(2); +const getFormattedPrice = (product: IPaywallProduct) => { + return (product?.trialPrice / 100).toFixed(2); }; export default function StepSubscriptionPlan() { const steps = useSteps(); const dispatch = useDispatch(); - const api = useApi(); - const { i18n } = useTranslation(); - const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan); - const allowedPlans = useMemo(() => [""], []); + const activeProductFromStore = useSelector(selectors.selectActiveProduct); + const { products } = usePaywall({ + placementKey: EPlacementKeys["aura.placement.palmistry.main"], + }); const storedEmail = steps.getStoredValue(Step.Email); - const [subscriptionPlan, setSubscriptionPlan] = React.useState(""); - const [subscriptionPlans, setSubscriptionPlans] = React.useState< - ISubscriptionPlan[] - >([]); + const [product, setProduct] = React.useState(""); const [email, setEmail] = React.useState(steps.getStoredValue(Step.Email)); - const locale = i18n.language; - React.useEffect(() => { - if (activeSubPlanFromStore) { - setSubscriptionPlan(activeSubPlanFromStore.id); + if (activeProductFromStore) { + setProduct(activeProductFromStore._id); } - }, [activeSubPlanFromStore]); - - React.useEffect(() => { - (async () => { - const { sub_plans } = await api.getSubscriptionPlans({ locale }); - const plans = sub_plans - .filter( - (plan: ISubscriptionPlan) => - plan.provider === "stripe" && !plan.name.includes("(test)") - ) - .sort((a, b) => { - if (!a.trial || !b.trial) { - return 0; - } - if (a?.trial?.price_cents < b?.trial?.price_cents) { - return -1; - } - if (a?.trial?.price_cents > b?.trial?.price_cents) { - return 1; - } - return 0; - }); - setSubscriptionPlans( - plans.filter( - (plan) => plan.trial?.price_cents || allowedPlans.includes(plan.id) - ) - ); - })(); - }, [allowedPlans, api, locale]); - - React.useEffect(() => { - if (subscriptionPlan) { - const targetSubPlan = subscriptionPlans.find( - (sub_plan) => sub_plan.id === subscriptionPlan - ); - - if (targetSubPlan) { - dispatch(actions.payment.update({ activeSubPlan: targetSubPlan })); - } - } - }, [subscriptionPlan]); + }, [activeProductFromStore]); React.useEffect(() => { setEmail(storedEmail || ""); }, [storedEmail]); const onNext = () => { - steps.saveCurrent(subscriptionPlan); + const targetProduct = products.find((_product) => _product._id === product); + + if (targetProduct) { + dispatch(actions.payment.update({ activeProduct: targetProduct })); + } + steps.saveCurrent(product); steps.goNext(); }; @@ -146,24 +105,22 @@ export default function StepSubscriptionPlan() {
- {subscriptionPlans.map((plan) => ( + {products.map((_product) => (
setSubscriptionPlan(plan.id)} + onClick={() => setProduct(_product._id)} > -

${getFormattedPrice(plan)}

+

${getFormattedPrice(_product)}

))}
void; }; +const isProduction = import.meta.env.MODE === "production"; + export default function StepUpload(props: Props) { const steps = useSteps(); const api = useApi(); @@ -606,7 +608,11 @@ export default function StepUpload(props: Props) { className="palmistry-container__take-palm-button" disabled={isUpladProcessing} active={!isUpladProcessing} - onClick={() => setPalmCameraModalIsOpen(true)} + onClick={() => + isProduction + ? setPalmCameraModalIsOpen(true) + : setUploadMenuModalIsOpen(true) + } isProcessing={isUpladProcessing} > {(isUpladProcessing && "Loading photo") || "Take a picture now"} diff --git a/src/components/palmistry/stripe-form/stripe-form.tsx b/src/components/palmistry/stripe-form/stripe-form.tsx index a6b07de..fc10342 100644 --- a/src/components/palmistry/stripe-form/stripe-form.tsx +++ b/src/components/palmistry/stripe-form/stripe-form.tsx @@ -1,19 +1,20 @@ -import React from 'react'; -import { StripeError } from '@stripe/stripe-js'; +import React from "react"; +import { StripeError } from "@stripe/stripe-js"; import { PaymentElement, useElements, useStripe, -} from '@stripe/react-stripe-js'; +} from "@stripe/react-stripe-js"; -import './stripe-form.css'; +import "./stripe-form.css"; -import Button from '../button/button'; +import Button from "../button/button"; type Props = { subscriptionReceiptId: string; isProcessing: boolean; paymentResultUrl: string; + confirmType: "payment" | "setup"; onSubmit: () => void; onSuccess: () => void; onError: (error: StripeError) => void; @@ -22,6 +23,7 @@ type Props = { export default function StripeForm(props: Props) { const stripe = useStripe(); const elements = useElements(); + const confirmType = props.confirmType || "payment"; const [formReady, setFormReady] = React.useState(false); @@ -33,7 +35,9 @@ export default function StripeForm(props: Props) { props.onSubmit(); try { - const { error } = await stripe.confirmPayment({ + const { error } = await stripe[ + confirmType === "payment" ? "confirmPayment" : "confirmSetup" + ]({ elements, confirmParams: { return_url: props.paymentResultUrl, @@ -52,10 +56,14 @@ export default function StripeForm(props: Props) { return (
- setFormReady(true)}/> + setFormReady(true)} />
-
diff --git a/src/env.d.ts b/src/env.d.ts index d15030a..c7ddc5d 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -1,6 +1,7 @@ interface ImportMetaEnv { AURA_API_HOST: string, AURA_DAPI_HOST: string, + AURA_DAPI_PREFIX: string, AURA_SITE_HOST: string, AURA_PREFIX: string, AURA_OPEN_AI_HOST: number, diff --git a/src/hooks/payment/useMakePayment.ts b/src/hooks/payment/useMakePayment.ts new file mode 100644 index 0000000..f1a0469 --- /dev/null +++ b/src/hooks/payment/useMakePayment.ts @@ -0,0 +1,79 @@ +import { useApi } from "@/api"; +import { selectors } from "@/store"; +import { useCallback, useEffect, useMemo, useState } from "react" +import { useSelector } from "react-redux"; + +interface IUseMakePaymentProps { + productId: string; + returnPaidUrl?: string; +} + +export const useMakePayment = ({ productId, returnPaidUrl = `https://${window.location.host}/payment/result/` }: IUseMakePaymentProps) => { + const api = useApi(); + const token = useSelector(selectors.selectToken); + const [paymentIntentId, setPaymentIntentId] = useState(); + const [paymentType, setPaymentType] = useState<"payment" | "setup">("payment"); + const [clientSecret, setClientSecret] = useState(); + const [publicKey, setPublicKey] = useState(); + const [returnUrl, setReturnUrl] = useState(); + const [error, setError] = useState(); + const [isLoading, setIsLoading] = useState(false); + + const makePayment = useCallback(async () => { + if (!productId?.length) { + return; + } + try { + setIsLoading(true); + const res = await api.makePayment({ + token, + productId + }); + + if (res.status === "paid") { + return window.location.href = `${returnPaidUrl}?redirect_status=succeeded`; + } + + if ("message" in res && res.message) { + return setError(res.message); + } + + if (!("data" in res) || !res.data) { + return; + } + const { data, type } = res; + setPaymentIntentId(data.paymentIntentId); + setPaymentType(type); + setClientSecret(data.client_secret); + setReturnUrl(data.return_url); + setPublicKey(data.public_key); + } catch (error) { + setError(error as string); + } finally { + setIsLoading(false); + } + }, [api, productId, returnPaidUrl, token]) + + useEffect(() => { + makePayment() + }, [makePayment]) + + + return useMemo(() => ({ + paymentIntentId, + paymentType, + clientSecret, + returnUrl, + publicKey, + error, + isLoading + }), [ + clientSecret, + error, + isLoading, + paymentIntentId, + paymentType, + publicKey, + returnUrl + ]) +} \ No newline at end of file diff --git a/src/hooks/payment/useSinglePayment.ts b/src/hooks/payment/useSinglePayment.ts index df40126..83a3807 100644 --- a/src/hooks/payment/useSinglePayment.ts +++ b/src/hooks/payment/useSinglePayment.ts @@ -130,7 +130,7 @@ export const useSinglePayment = () => { age: null, }, paymentInfo: { - productId: product?.productId || "", + productId: product?._id || "", key: product?.key || "", }, return_url: returnUrl, diff --git a/src/hooks/paywall/defaultPaywalls.ts b/src/hooks/paywall/defaultPaywalls.ts new file mode 100644 index 0000000..3244540 --- /dev/null +++ b/src/hooks/paywall/defaultPaywalls.ts @@ -0,0 +1,449 @@ +import { EPlacementKeys, IPaywall } from "@/api/resources/Paywall"; + +export const defaultPaywalls: { [key in EPlacementKeys]: IPaywall } = { + "aura.placement.redesign.main": { + "_id": "664215e2859ff1199d3a6e9a", + "key": "aura.paywall.redesign.main", + "name": "Main redesign paywall without free trial", + "products": [ + { + "_id": "65ff043dfc0fcfc4be550035", + "key": "compatibility.pdf.trial.0", + "productId": "prod_PnStTEBzrPLgvL", + "name": "Сompatibility AURA | Trial $0.99", + "priceId": "price_1PG2RSIlX4lgwUxrDfU2BDS4", + "type": "subscription", + "description": "Description", + "discountPrice": null, + "discountPriceId": null, + "isDiscount": false, + "isFreeTrial": false, + "isTrial": true, + "price": 1900, + "trialDuration": 3, + "trialPrice": 99, + "trialPriceId": "price_1PFiSkIlX4lgwUxrVel0l445" + }, + { + "_id": "66420b6e859ff1199d3a6e88", + "key": "compatibility.pdf.trial.1", + "productId": "prod_PnStTEBzrPLgvL", + "name": "Сompatibility AURA | Trial $5.00", + "priceId": "price_1PG2RSIlX4lgwUxrDfU2BDS4", + "type": "subscription", + "description": "Description", + "discountPrice": null, + "discountPriceId": null, + "isDiscount": false, + "isFreeTrial": false, + "isTrial": true, + "price": 1900, + "trialDuration": 3, + "trialPrice": 500, + "trialPriceId": "price_1PFyASIlX4lgwUxriLtzsk05" + }, + { + "_id": "66420be1859ff1199d3a6e89", + "key": "compatibility.pdf.trial.2", + "productId": "prod_PnStTEBzrPLgvL", + "name": "Сompatibility AURA | Trial $9.00", + "priceId": "price_1PG2RSIlX4lgwUxrDfU2BDS4", + "type": "subscription", + "description": "Description", + "discountPrice": null, + "discountPriceId": null, + "isDiscount": false, + "isFreeTrial": false, + "isTrial": true, + "price": 1900, + "trialDuration": 3, + "trialPrice": 900, + "trialPriceId": "price_1PFyAuIlX4lgwUxr4fFoauCV" + }, + { + "_id": "66420c1c859ff1199d3a6e8a", + "key": "compatibility.pdf.trial.3", + "productId": "prod_PnStTEBzrPLgvL", + "name": "Сompatibility AURA | Trial $13.76", + "priceId": "price_1PG2RSIlX4lgwUxrDfU2BDS4", + "type": "subscription", + "description": "Description", + "discountPrice": null, + "discountPriceId": null, + "isDiscount": false, + "isFreeTrial": false, + "isTrial": true, + "price": 1900, + "trialDuration": 3, + "trialPrice": 1376, + "trialPriceId": "price_1PFyBQIlX4lgwUxrMnEUkV73" + } + ], + "properties": [ + { + "key": "text.0", + "value": "We've helped millions of people to have happier lives and better relationships, and we want to help you too.", + "_id": "664542bbfe0a8eb4ee0b4f27" + }, + { + "key": "text.0.color", + "value": "millions", + "_id": "664542bbfe0a8eb4ee0b4f28" + }, + { + "key": "text.1", + "value": "Money shouldn’t stand in the way of finding astrology guidance that finally works. So, choose an amount that you think is reasonable to try us out for one week.", + "_id": "664542bbfe0a8eb4ee0b4f29" + }, + { + "key": "text.2", + "value": "It costs us $13.67 to offer a 3-day trial, but please choose the amount you are comfortable with.", + "_id": "664542bbfe0a8eb4ee0b4f2a" + }, + { + "key": "text.3", + "value": "This option will help us support those who need to select the lowest trial prices!", + "_id": "664542bbfe0a8eb4ee0b4f2b" + }, + { + "key": "text.4", + "value": "*Cost of trial as of February 2024", + "_id": "664542bbfe0a8eb4ee0b4f2c" + }, + { + "key": "text.5", + "value": "${quantity} people joined today", + "_id": "664542bbfe0a8eb4ee0b4f2d" + }, + { + "key": "text.button.1", + "value": "See my plan", + "_id": "664542bbfe0a8eb4ee0b4f2e" + } + ] + }, + "aura.placement.main": { + "_id": "664215e2859ff1199d3a6e9a", + "key": "aura.paywall.redesign.main", + "name": "Main redesign paywall without free trial", + "products": [ + { + "_id": "65ff043dfc0fcfc4be550035", + "key": "compatibility.pdf.trial.0", + "productId": "prod_PnStTEBzrPLgvL", + "name": "Сompatibility AURA | Trial $0.99", + "priceId": "price_1PG2RSIlX4lgwUxrDfU2BDS4", + "type": "subscription", + "description": "Description", + "discountPrice": null, + "discountPriceId": null, + "isDiscount": false, + "isFreeTrial": false, + "isTrial": true, + "price": 1900, + "trialDuration": 3, + "trialPrice": 99, + "trialPriceId": "price_1PFiSkIlX4lgwUxrVel0l445" + }, + { + "_id": "66420b6e859ff1199d3a6e88", + "key": "compatibility.pdf.trial.1", + "productId": "prod_PnStTEBzrPLgvL", + "name": "Сompatibility AURA | Trial $5.00", + "priceId": "price_1PG2RSIlX4lgwUxrDfU2BDS4", + "type": "subscription", + "description": "Description", + "discountPrice": null, + "discountPriceId": null, + "isDiscount": false, + "isFreeTrial": false, + "isTrial": true, + "price": 1900, + "trialDuration": 3, + "trialPrice": 500, + "trialPriceId": "price_1PFyASIlX4lgwUxriLtzsk05" + }, + { + "_id": "66420be1859ff1199d3a6e89", + "key": "compatibility.pdf.trial.2", + "productId": "prod_PnStTEBzrPLgvL", + "name": "Сompatibility AURA | Trial $9.00", + "priceId": "price_1PG2RSIlX4lgwUxrDfU2BDS4", + "type": "subscription", + "description": "Description", + "discountPrice": null, + "discountPriceId": null, + "isDiscount": false, + "isFreeTrial": false, + "isTrial": true, + "price": 1900, + "trialDuration": 3, + "trialPrice": 900, + "trialPriceId": "price_1PFyAuIlX4lgwUxr4fFoauCV" + }, + { + "_id": "66420c1c859ff1199d3a6e8a", + "key": "compatibility.pdf.trial.3", + "productId": "prod_PnStTEBzrPLgvL", + "name": "Сompatibility AURA | Trial $13.76", + "priceId": "price_1PG2RSIlX4lgwUxrDfU2BDS4", + "type": "subscription", + "description": "Description", + "discountPrice": null, + "discountPriceId": null, + "isDiscount": false, + "isFreeTrial": false, + "isTrial": true, + "price": 1900, + "trialDuration": 3, + "trialPrice": 1376, + "trialPriceId": "price_1PFyBQIlX4lgwUxrMnEUkV73" + } + ], + "properties": [ + { + "key": "text.0", + "value": "We've helped millions of people to have happier lives and better relationships, and we want to help you too.", + "_id": "664542bbfe0a8eb4ee0b4f27" + }, + { + "key": "text.0.color", + "value": "millions", + "_id": "664542bbfe0a8eb4ee0b4f28" + }, + { + "key": "text.1", + "value": "Money shouldn’t stand in the way of finding astrology guidance that finally works. So, choose an amount that you think is reasonable to try us out for one week.", + "_id": "664542bbfe0a8eb4ee0b4f29" + }, + { + "key": "text.2", + "value": "It costs us $13.67 to offer a 3-day trial, but please choose the amount you are comfortable with.", + "_id": "664542bbfe0a8eb4ee0b4f2a" + }, + { + "key": "text.3", + "value": "This option will help us support those who need to select the lowest trial prices!", + "_id": "664542bbfe0a8eb4ee0b4f2b" + }, + { + "key": "text.4", + "value": "*Cost of trial as of February 2024", + "_id": "664542bbfe0a8eb4ee0b4f2c" + }, + { + "key": "text.5", + "value": "${quantity} people joined today", + "_id": "664542bbfe0a8eb4ee0b4f2d" + }, + { + "key": "text.button.1", + "value": "See my plan", + "_id": "664542bbfe0a8eb4ee0b4f2e" + } + ] + }, + "aura.placement.email.marketing": { + "_id": "66565b15e4ad13456f7d04f2", + "key": "aura.paywall.email.marketing", + "name": "Special Offer!", + "products": [ + { + "_id": "66565aa8e4ad13456f7d02a6", + "key": "compatibility.pdf.trial.5", + "productId": "prod_PnStTEBzrPLgvL", + "priceId": "price_1PG2RSIlX4lgwUxrDfU2BDS4", + "name": "Сompatibility AURA | Trial $0.50", + "description": "Description", + "price": 1900, + "discountPrice": null, + "isDiscount": false, + "discountPriceId": null, + "trialDuration": 3, + "trialPrice": 50, + "isFreeTrial": false, + "isTrial": true, + "trialPriceId": "price_1PG1vKIlX4lgwUxrVhBf6eIq", + "type": "subscription" + } + ], + "properties": [ + { + "key": "text.old.price", + "value": "up to $13.67", + "_id": "66565b15e4ad13456f7d04f3" + } + ] + }, + "aura.placement.secret.discount": { + "_id": "6658973fef0d180993cdbcc1", + "key": "aura.paywall.secret.discount", + "name": "Secret Discount", + "products": [ + { + "_id": "66589439ef0d180993cdb72f", + "key": "compatibility.secret.discount.trial.0", + "productId": "prod_PnStTEBzrPLgvL", + "priceId": "price_1PMAREIlX4lgwUxrmvtHWth1", + "name": "Сompatibility AURA Secret Discount | Trial $0.99", + "description": "Description", + "price": 900, + "discountPrice": null, + "isDiscount": false, + "discountPriceId": null, + "trialDuration": 3, + "trialPrice": 99, + "isFreeTrial": false, + "isTrial": true, + "trialPriceId": "price_1PFiSkIlX4lgwUxrVel0l445", + "type": "subscription" + }, + { + "_id": "6658946cef0d180993cdb79f", + "key": "compatibility.secret.discount.trial.1", + "productId": "prod_PnStTEBzrPLgvL", + "priceId": "price_1PMAREIlX4lgwUxrmvtHWth1", + "name": "Сompatibility AURA Secret Discount | Trial $5.00", + "description": "Description", + "price": 900, + "discountPrice": null, + "isDiscount": false, + "discountPriceId": null, + "trialDuration": 3, + "trialPrice": 500, + "isFreeTrial": false, + "isTrial": true, + "trialPriceId": "price_1PFyASIlX4lgwUxriLtzsk05", + "type": "subscription" + }, + { + "_id": "66589544ef0d180993cdb7dc", + "key": "compatibility.secret.discount.trial.2", + "productId": "prod_PnStTEBzrPLgvL", + "priceId": "price_1PMAREIlX4lgwUxrmvtHWth1", + "name": "Сompatibility AURA Secret Discount | Trial $9.00", + "description": "Description", + "price": 900, + "discountPrice": null, + "isDiscount": false, + "discountPriceId": null, + "trialDuration": 3, + "trialPrice": 900, + "isFreeTrial": false, + "isTrial": true, + "trialPriceId": "price_1PFyAuIlX4lgwUxr4fFoauCV", + "type": "subscription" + }, + { + "_id": "66589591ef0d180993cdb88e", + "key": "compatibility.secret.discount.trial.3", + "productId": "prod_PnStTEBzrPLgvL", + "priceId": "price_1PMAREIlX4lgwUxrmvtHWth1", + "name": "Сompatibility AURA Secret Discount | Trial $13.76", + "description": "Description", + "price": 900, + "discountPrice": null, + "isDiscount": false, + "discountPriceId": null, + "trialDuration": 3, + "trialPrice": 1376, + "isFreeTrial": false, + "isTrial": true, + "trialPriceId": "price_1PFyBQIlX4lgwUxrMnEUkV73", + "type": "subscription" + } + ], + "properties": [] + }, + "aura.placement.palmistry.main": { + "_id": "66565a0ee4ad13456f7d0079", + "key": "aura.paywall.palmistry.main", + "name": "paywall without free trial", + "products": [ + { + "_id": "65ff043dfc0fcfc4be550035", + "key": "compatibility.pdf.trial.0", + "productId": "prod_PnStTEBzrPLgvL", + "name": "Сompatibility AURA | Trial $0.99", + "priceId": "price_1PG2RSIlX4lgwUxrDfU2BDS4", + "type": "subscription", + "description": "Description", + "discountPrice": null, + "discountPriceId": null, + "isDiscount": false, + "isFreeTrial": false, + "isTrial": true, + "price": 1900, + "trialDuration": 3, + "trialPrice": 99, + "trialPriceId": "price_1PFiSkIlX4lgwUxrVel0l445" + }, + { + "_id": "66420b6e859ff1199d3a6e88", + "key": "compatibility.pdf.trial.1", + "productId": "prod_PnStTEBzrPLgvL", + "name": "Сompatibility AURA | Trial $5.00", + "priceId": "price_1PG2RSIlX4lgwUxrDfU2BDS4", + "type": "subscription", + "description": "Description", + "discountPrice": null, + "discountPriceId": null, + "isDiscount": false, + "isFreeTrial": false, + "isTrial": true, + "price": 1900, + "trialDuration": 3, + "trialPrice": 500, + "trialPriceId": "price_1PFyASIlX4lgwUxriLtzsk05" + }, + { + "_id": "66420be1859ff1199d3a6e89", + "key": "compatibility.pdf.trial.2", + "productId": "prod_PnStTEBzrPLgvL", + "name": "Сompatibility AURA | Trial $9.00", + "priceId": "price_1PG2RSIlX4lgwUxrDfU2BDS4", + "type": "subscription", + "description": "Description", + "discountPrice": null, + "discountPriceId": null, + "isDiscount": false, + "isFreeTrial": false, + "isTrial": true, + "price": 1900, + "trialDuration": 3, + "trialPrice": 900, + "trialPriceId": "price_1PFyAuIlX4lgwUxr4fFoauCV" + }, + { + "_id": "66420c1c859ff1199d3a6e8a", + "key": "compatibility.pdf.trial.3", + "productId": "prod_PnStTEBzrPLgvL", + "name": "Сompatibility AURA | Trial $13.76", + "priceId": "price_1PG2RSIlX4lgwUxrDfU2BDS4", + "type": "subscription", + "description": "Description", + "discountPrice": null, + "discountPriceId": null, + "isDiscount": false, + "isFreeTrial": false, + "isTrial": true, + "price": 1900, + "trialDuration": 3, + "trialPrice": 1376, + "trialPriceId": "price_1PFyBQIlX4lgwUxrMnEUkV73" + } + ], + "properties": [ + { + "key": "text.0", + "value": "We've helped millions of people to\nreveal the destiny of their love life\nand what the future holds for them\nand their families.", + "_id": "664542bbfe0a8eb4ee0b4f27" + }, + { + "key": "text.1", + "value": "It costs us $13.21 to compensate our AURA\nemployees for the trial, but please choose the\namount you are comfortable with.", + "_id": "664542bbfe0a8eb4ee0b4f29" + } + ] + } +} \ No newline at end of file diff --git a/src/hooks/paywall/usePaywall.tsx b/src/hooks/paywall/usePaywall.tsx new file mode 100644 index 0000000..1bdc2fc --- /dev/null +++ b/src/hooks/paywall/usePaywall.tsx @@ -0,0 +1,124 @@ +import { useApi } from "@/api"; +import { EPlacementKeys, IPaywall } from "@/api/resources/Paywall"; +import { actions, selectors } from "@/store"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import parse from "html-react-parser"; +import { defaultPaywalls } from "./defaultPaywalls"; + +interface IUsePaywallProps { + placementKey: EPlacementKeys; +} + +interface IGetTextProps { + replacementSelector?: string; + color?: string; + replacement?: { + target: string; + replacement: string; + }; +} + +export function usePaywall({ placementKey }: IUsePaywallProps) { + const api = useApi(); + const dispatch = useDispatch(); + const token = useSelector(selectors.selectToken); + const [paywall, setPaywall] = useState(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(false); + const products = useMemo(() => paywall?.products || [], [paywall?.products]); + const properties = useMemo( + () => paywall?.properties || [], + [paywall?.properties] + ); + const isMustUpdate = useSelector(selectors.selectPaywallsIsMustUpdate); + const paywalls = useSelector(selectors.selectPaywalls); + + const getPaywallByPlacementKey = useCallback( + async (placementKey: EPlacementKeys) => { + try { + setIsLoading(true); + setError(false); + const paywall = await api.getPaywallByPlacementKey({ + placementKey, + token, + }); + if ("paywall" in paywall && paywall.paywall) { + setPaywall(paywall.paywall); + dispatch( + actions.paywalls.updatePaywall({ + [placementKey]: paywall.paywall, + }) + ); + dispatch( + actions.paywalls.updateIsMustUpdate({ + [placementKey]: false, + }) + ); + } + } catch (error) { + console.error(error); + setError(true); + setPaywall(defaultPaywalls[placementKey]); + } finally { + setIsLoading(false); + } + }, + [api, dispatch, token] + ); + + useEffect(() => { + if (isMustUpdate[placementKey] || !paywalls[placementKey]) { + getPaywallByPlacementKey(placementKey); + } else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + setPaywall(paywalls[placementKey]!); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getPaywallByPlacementKey, placementKey, isMustUpdate]); + + const getText = useCallback( + ( + key: string, + { + replacementSelector = "span", + color = "inherit", + replacement, + }: IGetTextProps + ) => { + const property = properties.find((property) => property.key === key); + if (!property) return ""; + const text = property.value; + const colorElements = properties.filter((property) => + property.key.includes(`${key}.color`) + ); + if (text && colorElements.length) { + let element = text; + for (const colorElement of colorElements) { + element = element.replace( + colorElement.value, + `<${replacementSelector} class="${property.key}" style="color: ${color}">${colorElement.value}` + ); + } + return parse(element); + } + if (text && replacement) { + return text.replace(replacement.target, replacement.replacement); + } + return text; + }, + [properties] + ); + + return useMemo( + () => ({ + paywall, + isLoading, + error, + products, + properties, + getText, + }), + [error, isLoading, paywall, products, properties, getText] + ); +} diff --git a/src/routes.ts b/src/routes.ts index f7a9d25..3cf0252 100755 --- a/src/routes.ts +++ b/src/routes.ts @@ -1,3 +1,4 @@ +import { EPlacementKeys } from "./api/resources/Paywall"; import type { UserStatus } from "./types"; const environments = import.meta.env @@ -5,6 +6,7 @@ const environments = import.meta.env const host = ""; export const apiHost = environments.AURA_API_HOST; const dApiHost = environments.AURA_DAPI_HOST +const dApiPrefix = environments.AURA_DAPI_PREFIX const siteHost = environments.AURA_SITE_HOST const prefix = environments.AURA_PREFIX; const openAIHost = environments.AURA_OPEN_AI_HOST; @@ -55,7 +57,6 @@ const routes = { paymentResult: () => [host, "payment", "result"].join("/"), paymentSuccess: () => [host, "payment", "success"].join("/"), paymentFail: () => [host, "payment", "fail"].join("/"), - paymentStripe: () => [host, "payment", "stripe"].join("/"), wallpaper: () => [host, "wallpaper"].join("/"), static: () => [host, "static", ":typeId"].join("/"), legal: (type: string) => [host, "static", type].join("/"), @@ -224,6 +225,8 @@ const routes = { [apiHost, prefix, "user", "subscription_receipts", "status.json"].join( "/" ), + subscriptionStatusNew: () => + [dApiHost, "users", "subscription", "status"].join("/"), subscriptionReceipts: () => [apiHost, prefix, "user", "subscription_receipts.json"].join("/"), subscriptionReceipt: (id: string) => @@ -262,6 +265,15 @@ const routes = { // Palmistry getPalmistryLines: () => ["https://api.aura.witapps.us", "palmistry", "lines"].join("/"), + + // Paywall + getPaywallByPlacementKey: (placementKey: EPlacementKeys) => + [dApiHost, dApiPrefix, "placement", placementKey, "paywall"].join("/"), + + // Payment + makePayment: () => + [dApiHost, dApiPrefix, "payment", "checkout"].join("/"), + }, openAi: { createThread: () => [openAIHost, openAiPrefix, "threads"].join("/"), @@ -338,7 +350,6 @@ export const withoutFooterRoutes = [ routes.client.paymentResult(), routes.client.paymentSuccess(), routes.client.paymentFail(), - routes.client.paymentStripe(), routes.client.magicBall(), routes.client.horoscopeBestiesResult(), routes.client.predictionMoonResult(), diff --git a/src/store/index.ts b/src/store/index.ts index fe822d4..868cc4d 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -14,6 +14,7 @@ import form, { selectors as formSelectors, } from "./form"; import aura, { actions as auraActions } from "./aura"; +import paywalls, { actions as paywallsActions } from "./paywalls"; import siteConfig, { selectHome, selectOpenAiToken, @@ -29,7 +30,7 @@ import onboardingConfig, { } from "./onboarding"; import payment, { actions as paymentActions, - selectActiveSubPlan, + selectActiveProduct, selectIsDiscount, selectSubscriptionReceipt, } from "./payment"; @@ -67,6 +68,7 @@ import palmistry, { actions as palmistryActions, selectPalmistryLines, } from "./palmistry"; +import { selectPaywallsIsMustUpdate, selectPaywalls } from "./paywalls"; const preloadedState = loadStore(); export const actions = { @@ -76,6 +78,7 @@ export const actions = { status: userStatusActions, subscriptionPlan: subscriptionPlasActions, aura: auraActions, + paywalls: paywallsActions, siteConfig: siteConfigActions, compatibility: compatibilityActions, compatibilities: compatibilitiesActions, @@ -97,7 +100,7 @@ export const selectors = { selectSelfName, selectCategoryId, selectSelectedPrice, - selectActiveSubPlan, + selectActiveProduct, selectUserCallbacksDescription, selectUserCallbacksPrevStat, selectHome, @@ -115,6 +118,8 @@ export const selectors = { selectIsForceShortPath, selectOpenAiToken, selectPalmistryLines, + selectPaywalls, + selectPaywallsIsMustUpdate, ...formSelectors, }; @@ -134,6 +139,7 @@ export const reducer = combineReducers({ questionnaire, userConfig, palmistry, + paywalls, }); export type RootState = ReturnType; diff --git a/src/store/payment.ts b/src/store/payment.ts index 2f7631e..23dfc9d 100644 --- a/src/store/payment.ts +++ b/src/store/payment.ts @@ -1,4 +1,4 @@ -import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans"; +import { IPaywallProduct } from "@/api/resources/Paywall"; import { SubscriptionReceipt } from "@/api/resources/UserSubscriptionReceipts"; import { createSlice, createSelector } from "@reduxjs/toolkit"; import type { PayloadAction } from "@reduxjs/toolkit"; @@ -6,15 +6,15 @@ import type { PayloadAction } from "@reduxjs/toolkit"; interface IPayment { selectedPrice: number | null; isDiscount: boolean; - activeSubPlan: ISubscriptionPlan | null; subscriptionReceipt: SubscriptionReceipt | null; + activeProduct: IPaywallProduct | null; } const initialState: IPayment = { selectedPrice: null, isDiscount: false, - activeSubPlan: null, subscriptionReceipt: null, + activeProduct: null, }; const paymentSlice = createSlice({ @@ -33,8 +33,8 @@ export const selectSelectedPrice = createSelector( (state: { payment: IPayment }) => state.payment.selectedPrice, (payment) => payment ); -export const selectActiveSubPlan = createSelector( - (state: { payment: IPayment }) => state.payment.activeSubPlan, +export const selectActiveProduct = createSelector( + (state: { payment: IPayment }) => state.payment.activeProduct, (payment) => payment ); export const selectIsDiscount = createSelector( diff --git a/src/store/paywalls.ts b/src/store/paywalls.ts new file mode 100644 index 0000000..d273f8b --- /dev/null +++ b/src/store/paywalls.ts @@ -0,0 +1,60 @@ +import { EPlacementKeys, IPaywall } from '@/api/resources/Paywall' +import { createSlice, createSelector } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' + +type TAdditionalParameters = { + isMustUpdate: { + [key in EPlacementKeys]: boolean + }; +} + +type TPaywalls = { + [key in EPlacementKeys]: IPaywall | null; +} & TAdditionalParameters + +type IPayloadUpdatePaywall = { + [key in EPlacementKeys]: IPaywall +} + +const initialState: TPaywalls = { + "aura.placement.main": null, + "aura.placement.redesign.main": null, + "aura.placement.email.marketing": null, + "aura.placement.secret.discount": null, + "aura.placement.palmistry.main": null, + isMustUpdate: { + "aura.placement.main": true, + "aura.placement.redesign.main": true, + "aura.placement.email.marketing": true, + "aura.placement.secret.discount": true, + "aura.placement.palmistry.main": true + }, +} + +const paywallsSlice = createSlice({ + name: 'paywalls', + initialState, + reducers: { + updatePaywall(state, action: PayloadAction>) { + return { ...state, ...action.payload } + }, + updateIsMustUpdate(state, action: PayloadAction>) { + return { ...state, isMustUpdate: { ...state.isMustUpdate, ...action.payload } } + }, + resetIsMustUpdate(state) { + return { ...state, isMustUpdate: initialState.isMustUpdate } + } + }, + extraReducers: (builder) => builder.addCase('reset', () => initialState), +}) + +export const { actions } = paywallsSlice +export const selectPaywalls = createSelector( + (state: { paywalls: TPaywalls }) => state.paywalls, + (paywalls) => paywalls +) +export const selectPaywallsIsMustUpdate = createSelector( + (state: { paywalls: TPaywalls }) => state.paywalls.isMustUpdate, + (paywalls) => paywalls +) +export default paywallsSlice.reducer