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/src/api/api.ts b/src/api/api.ts index 963151a..421d668 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,6 @@ const api = { getSubscriptionPlans: createMethod(SubscriptionPlans.createRequest), getSubscriptionCheckout: createMethod(SubscriptionCheckout.createRequest), getSubscriptionStatus: createMethod(SubscriptionStatus.createRequest), - getSubscriptionReceipt: createMethod(SubscriptionReceipts.createGetRequest), - createSubscriptionReceipt: createMethod(SubscriptionReceipts.createRequest), getAiCompatCategories: createMethod(AICompatCategories.createRequest), getAiCompat: createMethod(AICompats.createRequest), getAiRequest: createMethod(AIRequests.createRequest), @@ -75,6 +74,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..0aa0f77 --- /dev/null +++ b/src/api/resources/Paywall.ts @@ -0,0 +1,64 @@ +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", +} + +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/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/EmailEnterPage/index.tsx b/src/components/EmailEnterPage/index.tsx index 8ee524c..c54cddf 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)); @@ -122,7 +101,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) => (
| null>(null); - const [clientSecret, setClientSecret] = useState(""); - const [subscriptionReceiptId, setSubscriptionReceiptId] = - useState(""); - const [isLoading, setIsLoading] = useState(true); - if (!activeSubPlan) { + + const { products } = usePaywall({ + placementKey: EPlacementKeys["aura.placement.main"], + }); + + const activeProduct = useSelector(selectors.selectActiveProduct); + if (!activeProduct) { navigate(routes.client.trialChoice()); } + const { + paymentIntentId, + clientSecret, + returnUrl: checkoutUrl, + paymentType, + publicKey, + isLoading, + error, + } = useMakePayment({ + productId: activeProduct?._id || "", + }); + + if (checkoutUrl?.length) { + window.location.href = checkoutUrl; + } + 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 isActiveSubPlan = products.find( + (product) => product._id === activeProduct?._id ); - if (!activeSubPlan || !isActiveSubPlan) { + if (!activeProduct || !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]); + }, [activeProduct, navigate, products, publicKey]); const handleClose = () => { onClose(); }; + if (error?.length) { + return ( +
+ + Something went wrong + +
+ ); + } + return ( {isLoading ? ( @@ -97,17 +101,18 @@ StripeModalProps): JSX.Element {

{email}

)} - {stripePromise && clientSecret && subscriptionReceiptId && ( + {stripePromise && clientSecret && paymentIntentId && ( + {activeProduct && } + - {activeSubPlan && ( - - )} - )}
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/ApplePayButton/index.tsx b/src/components/StripePage/ApplePayButton/index.tsx index 02b1828..6e88090 100644 --- a/src/components/StripePage/ApplePayButton/index.tsx +++ b/src/components/StripePage/ApplePayButton/index.tsx @@ -5,14 +5,14 @@ import { useElements, } from "@stripe/react-stripe-js"; import { PaymentRequest } from "@stripe/stripe-js"; -import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans"; import styles from "./styles.module.css"; import { useDispatch } from "react-redux"; import { useNavigate } from "react-router-dom"; import routes from "@/routes"; +import { IPaywallProduct } from "@/api/resources/Paywall"; interface ApplePayButtonProps { - activeSubPlan: ISubscriptionPlan | null; + activeProduct: IPaywallProduct | null; client_secret: string; subscriptionReceiptId?: string; returnUrl?: string; @@ -20,7 +20,7 @@ interface ApplePayButtonProps { } function ApplePayButton({ - activeSubPlan, + activeProduct, client_secret, subscriptionReceiptId, returnUrl, @@ -34,15 +34,15 @@ function ApplePayButton({ null ); - const getAmountFromSubPlan = (subPlan: ISubscriptionPlan) => { - 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/index.tsx b/src/components/StripePage/index.tsx index 5b6336e..d33bae7 100644 --- a/src/components/StripePage/index.tsx +++ b/src/components/StripePage/index.tsx @@ -1,10 +1,8 @@ -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"; @@ -12,58 +10,63 @@ 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"; +import { usePaywall } from "@/hooks/paywall/usePaywall"; +import { EPlacementKeys } from "@/api/resources/Paywall"; +import { useMakePayment } from "@/hooks/payment/useMakePayment"; 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 activeProduct = useSelector(selectors.selectActiveProduct); 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) { + if (!activeProduct) { 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]); + const { + paymentIntentId, + clientSecret, + returnUrl: checkoutUrl, + paymentType, + publicKey, + isLoading, + error, + } = useMakePayment({ + productId: activeProduct?._id || "", + }); + + if (checkoutUrl?.length) { + window.location.href = checkoutUrl; + } + + const { products } = usePaywall({ + placementKey: EPlacementKeys["aura.placement.main"], + }); 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); + if (!products?.length || !publicKey) return; + setStripePromise(loadStripe(publicKey)); + const isActiveProduct = products.find( + (product) => product._id === activeProduct?._id + ); + if (!activeProduct || !isActiveProduct) { + navigate(routes.client.priceList()); + } })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [api, token]); + }, [activeProduct, navigate, products, publicKey]); + + if (error?.length) { + return ( +
+ + Something went wrong + +
+ ); + } return (
@@ -80,17 +83,18 @@ export function StripePage(): JSX.Element {

{email}

)} - {stripePromise && clientSecret && subscriptionReceiptId && ( + {stripePromise && clientSecret && paymentIntentId && ( + {activeProduct && } + - {activeSubPlan && ( - - )} - )}
diff --git a/src/components/SubPlanInformation/index.tsx b/src/components/SubPlanInformation/index.tsx index 5d991fa..1225348 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 { 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) => ( ([]); - 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)); @@ -127,7 +106,7 @@ function EmailEnterPage({ await authorization(email, source); dispatch( actions.payment.update({ - activeSubPlan, + activeProduct, }) ); setIsAuth(true); 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([]); 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} />
-

- We've helped{" "} - - millions - {" "} - of people to have happier lives and better relationships, and we want to - help you too. -

-

- 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. -

-

- It costs us $13.67 to offer a 3-day trial, but please choose the amount - you are comfortable with. -

-
- -

- This option will help us support those who need to select the lowest - trial prices! -

- {`Arrow -
-
- -
-

{email}

- - See my plan - -

- *Cost of trial as of February 2024 -

+ {!isLoading && ( + <> +

+ {getText("text.0", { + replacementSelector: "b", + color: "#1C38EA", + })} +

+

+ {getText("text.1", { + color: "#1C38EA", + })} +

+

+ {getText("text.2", { + color: "#1C38EA", + })} +

+
+ +

+ {getText("text.3", { + color: "#1C38EA", + })} +

+ {`Arrow +
+
+ +
+

{email}

+ + {getText("text.button.1", { + color: "#1C38EA", + })} + +

+ {getText("text.4", { + color: "#1C38EA", + })} +

+ + )} + {isLoading && } ); } 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/PaymentModal/index.tsx b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PaymentModal/index.tsx index 603f7ee..97de607 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 @@ -9,48 +9,56 @@ import CheckoutForm from "@/components/PaymentPage/methods/Stripe/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 | null>(null); - const [clientSecret, setClientSecret] = useState(""); - const [subscriptionReceiptId, setSubscriptionReceiptId] = - useState(""); - const [isLoading, setIsLoading] = useState(true); - const [isError, setIsError] = useState(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 (
@@ -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,9 +140,9 @@ function PaymentModal({ {selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && ( <div className={styles["payment-method"]}> <ApplePayButton - activeSubPlan={activeSubPlan} + activeProduct={_activeProduct} client_secret={clientSecret} - subscriptionReceiptId={subscriptionReceiptId} + subscriptionReceiptId={paymentIntentId} returnUrl={window.location.href} /> </div> @@ -170,7 +150,8 @@ function PaymentModal({ {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/index.tsx b/src/components/pages/ABDesign/v1/pages/TrialPayment/index.tsx index 72073e2..abb7364 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,22 @@ 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 (["relationship", "married"].includes(flowChoice)) { @@ -103,7 +79,7 @@ function TrialPaymentPage() { setSingleOrWithPartner("single"); }, [flowChoice]); - if (!activeSubPlan) { + if (!activeProduct) { return ; } @@ -168,7 +144,7 @@ function TrialPaymentPage() { 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..c8452b5 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!

@@ -34,7 +45,7 @@ function PaymentDiscountTable() {

Total today:

- {activeSub && ${getPriceFromTrial(activeSub.trial)}} + {activeProduct && ${getPrice(activeProduct)}}
); diff --git a/src/components/pages/EmailLetters/MarketingTrialPayment/index.tsx b/src/components/pages/EmailLetters/MarketingTrialPayment/index.tsx index fbc5f27..5f595d2 100644 --- a/src/components/pages/EmailLetters/MarketingTrialPayment/index.tsx +++ b/src/components/pages/EmailLetters/MarketingTrialPayment/index.tsx @@ -5,29 +5,31 @@ 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, IPaywallProduct } from "@/api/resources/Paywall"; function MarketingTrialPayment() { const { i18n } = useTranslation(); const locale = i18n.language; const api = useApi(); const [isOpenPaymentModal, setIsOpenPaymentModal] = useState(false); - const [freeTrialPlan, setFreeTrialPlan] = useState< - ISubscriptionPlan | undefined + const [freeTrialProduct, setFreeTrialProduct] = useState< + IPaywallProduct | undefined >(); + const { products } = usePaywall({ + placementKey: EPlacementKeys["aura.placement.main"], + }); + // get free trial plan useEffect(() => { (async () => { - const { sub_plans } = await api.getSubscriptionPlans({ locale }); - const _freeTrialPlan = sub_plans.find( - (subPlan) => subPlan.trial?.is_free - ); - setFreeTrialPlan(_freeTrialPlan); + const _freeProduct = products.find((product) => product.isFreeTrial); + setFreeTrialProduct(_freeProduct); })(); - }, [api, locale]); + }, [api, locale, products]); const openStripeModal = () => { setIsOpenPaymentModal(true); @@ -44,7 +46,7 @@ function MarketingTrialPayment() { open={isOpenPaymentModal} onClose={handleCloseModal} > - +
diff --git a/src/components/pages/PaymentWithEmailPage/PaymentForm/index.tsx b/src/components/pages/PaymentWithEmailPage/PaymentForm/index.tsx index bb6a583..aaa0019 100644 --- a/src/components/pages/PaymentWithEmailPage/PaymentForm/index.tsx +++ b/src/components/pages/PaymentWithEmailPage/PaymentForm/index.tsx @@ -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/TrialChoice/index.tsx b/src/components/pages/TrialChoice/index.tsx index 7507f42..257cf30 100755 --- a/src/components/pages/TrialChoice/index.tsx +++ b/src/components/pages/TrialChoice/index.tsx @@ -1,73 +1,36 @@ import PriceList from "@/components/PriceList"; 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"; import routes from "@/routes"; import EmailsList from "@/components/EmailsList"; import MainButton from "@/components/MainButton"; - -interface IPlanKey { - [key: string]: number; -} +import { usePaywall } from "@/hooks/paywall/usePaywall"; +import { EPlacementKeys } from "@/api/resources/Paywall"; +import { getRandomArbitrary } from "@/services/random-value"; 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([]); 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 603f7ee..c93f520 100644 --- a/src/components/pages/TrialPayment/components/PaymentModal/index.tsx +++ b/src/components/pages/TrialPayment/components/PaymentModal/index.tsx @@ -9,43 +9,58 @@ import CheckoutForm from "@/components/PaymentPage/methods/Stripe/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; } +const getPrice = (product: IPaywallProduct | null) => { + if (!product) { + return 0; + } + return (product.trialPrice === 100 ? 99 : product.trialPrice || 0) / 100; +}; + function PaymentModal({ - activeSubscriptionPlan, + activeProduct, noTrial, returnUrl, }: 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: EPlacementKeys["aura.placement.main"], + }); + 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 +79,16 @@ 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 (!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.trialChoice()); } })(); - }, [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 +100,7 @@ function PaymentModal({ ); } - if (isError) { + if (error?.length) { return (
@@ -132,16 +120,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,9 +145,9 @@ function PaymentModal({ {selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && ( <div className={styles["payment-method"]}> <ApplePayButton - activeSubPlan={activeSubPlan} + activeProduct={_activeProduct} client_secret={clientSecret} - subscriptionReceiptId={subscriptionReceiptId} + subscriptionReceiptId={paymentIntentId} returnUrl={window.location.href} /> </div> @@ -170,7 +155,8 @@ function PaymentModal({ {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..8dd84d3 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,36 @@ 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 (["relationship", "married"].includes(flowChoice)) { @@ -102,7 +78,7 @@ function TrialPaymentPage() { setMarginTopTitle(340); }, [flowChoice]); - if (!activeSubPlan) { + if (!activeProduct) { return ; } @@ -157,7 +133,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..c8452b5 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!

@@ -34,7 +45,7 @@ function PaymentDiscountTable() {

Total today:

- {activeSub && ${getPriceFromTrial(activeSub.trial)}} + {activeProduct && ${getPrice(activeProduct)}}
); diff --git a/src/components/palmistry/payment-screen/payment-screen.tsx b/src/components/palmistry/payment-screen/payment-screen.tsx index 2895207..7d577cd 100644 --- a/src/components/palmistry/payment-screen/payment-screen.tsx +++ b/src/components/palmistry/payment-screen/payment-screen.tsx @@ -2,21 +2,21 @@ 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"; const getFormattedPrice = (price: number) => { return (price / 100).toFixed(2); -} +}; export default function PaymentScreen() { const time = useTimer(); - const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan); + const activeProductFromStore = useSelector(selectors.selectActiveProduct); const subscriptionStatus = useSelector(selectors.selectStatus); const steps = useSteps(); @@ -27,18 +27,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 +238,17 @@ export default function PaymentScreen() { - {activeSubPlanFromStore && ( -
- {subscriptionStatus !== "subscribed" && } + {activeProductFromStore && ( +
+ {subscriptionStatus !== "subscribed" && ( + + )} {subscriptionStatus === "subscribed" && (
@@ -255,7 +265,9 @@ export default function PaymentScreen() { /> -
Payment success
+
+ Payment success +
)}
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..f59b3fe 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,58 @@ -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.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]); + }, [activeProductFromStore]); 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 (product) { + const targetProduct = products.find( + (_product) => _product._id === product ); - if (targetSubPlan) { - dispatch(actions.payment.update({ activeSubPlan: targetSubPlan })); + if (targetProduct) { + dispatch(actions.payment.update({ activeProduct: targetProduct })); } } - }, [subscriptionPlan]); + }, [dispatch, product, products]); React.useEffect(() => { setEmail(storedEmail || ""); }, [storedEmail]); const onNext = () => { - steps.saveCurrent(subscriptionPlan); + steps.saveCurrent(product); steps.goNext(); }; @@ -146,24 +112,22 @@ export default function StepSubscriptionPlan() {
- {subscriptionPlans.map((plan) => ( + {products.map((_product) => (
setSubscriptionPlan(plan.id)} + onClick={() => setProduct(_product._id)} > -

${getFormattedPrice(plan)}

+

${getFormattedPrice(_product)}

))}
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/paywall/usePaywall.tsx b/src/hooks/paywall/usePaywall.tsx new file mode 100644 index 0000000..8970747 --- /dev/null +++ b/src/hooks/paywall/usePaywall.tsx @@ -0,0 +1,103 @@ +import { useApi } from "@/api"; +import { EPlacementKeys, IPaywall } from "@/api/resources/Paywall"; +import { selectors } from "@/store"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useSelector } from "react-redux"; +import parse from "html-react-parser"; + +interface IUsePaywallProps { + placementKey: EPlacementKeys; +} + +interface IGetTextProps { + replacementSelector?: string; + color?: string; + replacement?: { + target: string; + replacement: string; + }; +} + +export function usePaywall({ placementKey }: IUsePaywallProps) { + const api = useApi(); + 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 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); + } + } catch (error) { + console.error(error); + setError(true); + } finally { + setIsLoading(false); + } + }, + [api, token] + ); + + useEffect(() => { + getPaywallByPlacementKey(placementKey); + }, [getPaywallByPlacementKey, placementKey]); + + 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..5f8996d 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; @@ -262,6 +264,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("/"), diff --git a/src/store/index.ts b/src/store/index.ts index fe822d4..6d23e08 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -29,7 +29,7 @@ import onboardingConfig, { } from "./onboarding"; import payment, { actions as paymentActions, - selectActiveSubPlan, + selectActiveProduct, selectIsDiscount, selectSubscriptionReceipt, } from "./payment"; @@ -97,7 +97,7 @@ export const selectors = { selectSelfName, selectCategoryId, selectSelectedPrice, - selectActiveSubPlan, + selectActiveProduct, selectUserCallbacksDescription, selectUserCallbacksPrevStat, selectHome, 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(