Merge branch 'develop' into 'main'

Develop

See merge request witapp/aura-webapp!154
This commit is contained in:
Daniil Chemerkin 2024-06-02 00:51:18 +00:00
commit 0b042eb02d
117 changed files with 1996 additions and 1676 deletions

View File

@ -1,5 +1,6 @@
AURA_API_HOST=https://api-web.aura.wit.life AURA_API_HOST=https://api-web.aura.wit.life
AURA_DAPI_HOST=https://dev.api.aura.witapps.us AURA_DAPI_HOST=https://dev.api.aura.witapps.us
AURA_DAPI_PREFIX=v2
AURA_SITE_HOST=https://aura.wit.life AURA_SITE_HOST=https://aura.wit.life
AURA_PREFIX=api/v1 AURA_PREFIX=api/v1
AURA_OPEN_AI_HOST=https://api.openai.com AURA_OPEN_AI_HOST=https://api.openai.com

View File

@ -1,5 +1,6 @@
AURA_API_HOST=https://api-web.aura.wit.life AURA_API_HOST=https://api-web.aura.wit.life
AURA_DAPI_HOST=https://api.aura.witapps.us AURA_DAPI_HOST=https://api.aura.witapps.us
AURA_DAPI_PREFIX=v2
AURA_SITE_HOST=https://aura.wit.life AURA_SITE_HOST=https://aura.wit.life
AURA_PREFIX=api/v1 AURA_PREFIX=api/v1
AURA_OPEN_AI_HOST=https://api.openai.com AURA_OPEN_AI_HOST=https://api.openai.com

View File

@ -6,7 +6,7 @@
name="viewport" name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/> />
<link rel="preload" as="image" href="/leo.png" fetchpriority="high" /> <link rel="preload" as="image" href="/leo.webp" fetchpriority="high" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
@ -69,7 +69,7 @@
</head> </head>
<body> <body>
<!-- Yandex.Metrika counter --> <!-- Yandex.Metrika counter -->
<script type="text/javascript" async="async"> <script type="text/javascript">
(function (m, e, t, r, i, k, a) { (function (m, e, t, r, i, k, a) {
m[i] = m[i] =
m[i] || m[i] ||
@ -133,7 +133,7 @@
<!-- End Google Tag Manager (noscript) --> <!-- End Google Tag Manager (noscript) -->
<div id="root"> <div id="root">
<div class="splash-screen"> <div class="splash-screen">
<img src="/leo.png" alt="Aura - Energy of your Horoscope" /> <img src="/leo.webp" alt="Aura - Energy of your Horoscope" />
</div> </div>
</div> </div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>

BIN
public/leo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -11,7 +11,6 @@ import {
DailyForecasts, DailyForecasts,
SubscriptionItems, SubscriptionItems,
SubscriptionCheckout, SubscriptionCheckout,
SubscriptionReceipts,
SubscriptionStatus, SubscriptionStatus,
AICompatCategories, AICompatCategories,
AICompats, AICompats,
@ -28,6 +27,8 @@ import {
SinglePayment, SinglePayment,
Products, Products,
Palmistry, Palmistry,
Paywall,
Payment,
} from './resources' } from './resources'
const api = { const api = {
@ -48,8 +49,8 @@ const api = {
getSubscriptionPlans: createMethod<SubscriptionPlans.Payload, SubscriptionPlans.Response>(SubscriptionPlans.createRequest), getSubscriptionPlans: createMethod<SubscriptionPlans.Payload, SubscriptionPlans.Response>(SubscriptionPlans.createRequest),
getSubscriptionCheckout: createMethod<SubscriptionCheckout.Payload, SubscriptionCheckout.Response>(SubscriptionCheckout.createRequest), getSubscriptionCheckout: createMethod<SubscriptionCheckout.Payload, SubscriptionCheckout.Response>(SubscriptionCheckout.createRequest),
getSubscriptionStatus: createMethod<SubscriptionStatus.Payload, SubscriptionStatus.Response>(SubscriptionStatus.createRequest), getSubscriptionStatus: createMethod<SubscriptionStatus.Payload, SubscriptionStatus.Response>(SubscriptionStatus.createRequest),
getSubscriptionReceipt: createMethod<SubscriptionReceipts.GetPayload, SubscriptionReceipts.Response>(SubscriptionReceipts.createGetRequest), // new get subscription status
createSubscriptionReceipt: createMethod<SubscriptionReceipts.Payload, SubscriptionReceipts.Response>(SubscriptionReceipts.createRequest), getSubscriptionStatusNew: createMethod<SubscriptionStatus.Payload, SubscriptionStatus.ResponseNew>(SubscriptionStatus.createRequestNew),
getAiCompatCategories: createMethod<AICompatCategories.Payload, AICompatCategories.Response>(AICompatCategories.createRequest), getAiCompatCategories: createMethod<AICompatCategories.Payload, AICompatCategories.Response>(AICompatCategories.createRequest),
getAiCompat: createMethod<AICompats.Payload, AICompats.Response>(AICompats.createRequest), getAiCompat: createMethod<AICompats.Payload, AICompats.Response>(AICompats.createRequest),
getAiRequest: createMethod<AIRequests.Payload, AIRequests.Response>(AIRequests.createRequest), getAiRequest: createMethod<AIRequests.Payload, AIRequests.Response>(AIRequests.createRequest),
@ -75,6 +76,10 @@ const api = {
getPalmistryLines: createMethod<Palmistry.Payload, Palmistry.Response>(Palmistry.createRequest), getPalmistryLines: createMethod<Palmistry.Payload, Palmistry.Response>(Palmistry.createRequest),
// New Authorization // New Authorization
authorization: createMethod<User.ICreateAuthorizePayload, User.ICreateAuthorizeResponse>(User.createAuthorizeRequest), authorization: createMethod<User.ICreateAuthorizePayload, User.ICreateAuthorizeResponse>(User.createAuthorizeRequest),
// Paywall
getPaywallByPlacementKey: createMethod<Paywall.PayloadGet, Paywall.ResponseGet>(Paywall.createRequestGet),
// Payment
makePayment: createMethod<Payment.PayloadPost, Payment.ResponsePost>(Payment.createRequestPost),
} }
export type ApiContextValue = typeof api export type ApiContextValue = typeof api

View File

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

View File

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

View File

@ -32,10 +32,18 @@ export interface PayloadPost extends Payload {
} }
export interface ResponseGet { export interface ResponseGet {
key: string; _id: string,
productId: string; key: string,
amount: number; name: string,
currency: string; type: string,
description: string,
discountPrice: null | unknown,
isDiscount: boolean,
isFreeTrial: boolean,
isTrial: boolean,
price: number,
trialDuration: number,
trialPrice: number
} }
interface ResponsePostNewPaymentData { interface ResponsePostNewPaymentData {

View File

@ -14,3 +14,12 @@ export const createRequest = ({ token }: Payload): Request => {
const url = new URL(routes.server.subscriptionStatus()) const url = new URL(routes.server.subscriptionStatus())
return new Request(url, { method: 'GET', headers: getAuthHeaders(token) }) 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) })
}

View File

@ -10,7 +10,6 @@ export * as AuthTokens from "./AuthTokens";
export * as SubscriptionItems from "./UserSubscriptionItemPrices"; export * as SubscriptionItems from "./UserSubscriptionItemPrices";
export * as SubscriptionCheckout from "./UserSubscriptionCheckout"; export * as SubscriptionCheckout from "./UserSubscriptionCheckout";
export * as SubscriptionStatus from "./UserSubscriptionStatus"; export * as SubscriptionStatus from "./UserSubscriptionStatus";
export * as SubscriptionReceipts from "./UserSubscriptionReceipts";
export * as AICompatCategories from "./AICompatCategories"; export * as AICompatCategories from "./AICompatCategories";
export * as AICompats from "./AICompats"; export * as AICompats from "./AICompats";
export * as AIRequests from "./AIRequests"; export * as AIRequests from "./AIRequests";
@ -26,3 +25,5 @@ export * as OpenAI from "./OpenAI";
export * as SinglePayment from "./SinglePayment"; export * as SinglePayment from "./SinglePayment";
export * as Products from "./Products"; export * as Products from "./Products";
export * as Palmistry from "./Palmistry"; export * as Palmistry from "./Palmistry";
export * as Paywall from "./Paywall";
export * as Payment from "./Payment";

View File

@ -57,7 +57,6 @@ import { Asset } from "@/api/resources/Assets";
import PaymentResultPage from "../PaymentPage/results"; import PaymentResultPage from "../PaymentPage/results";
import PaymentSuccessPage from "../PaymentPage/results/SuccessPage"; import PaymentSuccessPage from "../PaymentPage/results/SuccessPage";
import PaymentFailPage from "../PaymentPage/results/ErrorPage"; import PaymentFailPage from "../PaymentPage/results/ErrorPage";
import { StripePage } from "../StripePage";
import AuthPage from "../AuthPage"; import AuthPage from "../AuthPage";
import AuthResultPage from "../AuthResultPage"; import AuthResultPage from "../AuthResultPage";
import MagicBallPage from "../pages/MagicBall"; 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 StepsManager from "@/components/palmistry/steps-manager/steps-manager";
import Advisors from "../pages/Advisors"; import Advisors from "../pages/Advisors";
import AdvisorChatPage from "../pages/AdvisorChat"; import AdvisorChatPage from "../pages/AdvisorChat";
import SuccessPaymentPage from "../pages/PaymentWithEmailPage/ResultPayment/SuccessPaymentPage"; import SuccessPaymentPage from "../pages/SinglePaymentPage/ResultPayment/SuccessPaymentPage";
import FailPaymentPage from "../pages/PaymentWithEmailPage/ResultPayment/FailPaymentPage"; import FailPaymentPage from "../pages/SinglePaymentPage/ResultPayment/FailPaymentPage";
import { useSchemeColorByElement } from "@/hooks/useSchemeColorByElement"; import { useSchemeColorByElement } from "@/hooks/useSchemeColorByElement";
import GetInformationPartnerPage from "../pages/GetInformationPartner"; import GetInformationPartnerPage from "../pages/GetInformationPartner";
import BirthPlacePage from "../pages/BirthPlacePage"; import BirthPlacePage from "../pages/BirthPlacePage";
@ -162,6 +161,10 @@ function App(): JSX.Element {
const birthdate = user?.profile?.birthday || birthdateFromStore; const birthdate = user?.profile?.birthday || birthdateFromStore;
const birthPlace = user?.profile?.birthplace || birthPlaceFromStore; const birthPlace = user?.profile?.birthplace || birthPlaceFromStore;
useLayoutEffect(() => {
dispatch(actions.paywalls.resetIsMustUpdate());
}, [dispatch]);
useEffect(() => { useEffect(() => {
// api.getAppConfig({ bundleId: "auraweb" }), // api.getAppConfig({ bundleId: "auraweb" }),
dispatch( dispatch(
@ -219,10 +222,13 @@ function App(): JSX.Element {
token, token,
}); });
if (has_subscription && user) { const { subscription: subscriptionStatusNew } =
await api.getSubscriptionStatusNew({ token });
if ((has_subscription || subscriptionStatusNew) && user) {
return dispatch(actions.status.update("subscribed")); return dispatch(actions.status.update("subscribed"));
} }
if (!has_subscription && user) { if (!has_subscription && !subscriptionStatusNew && user) {
return dispatch(actions.status.update("unsubscribed")); return dispatch(actions.status.update("unsubscribed"));
} }
if (!user) { if (!user) {
@ -292,7 +298,9 @@ function App(): JSX.Element {
<Route <Route
path={routes.client.epeGender()} path={routes.client.epeGender()}
element={<GenderPage productKey={EProductKeys["moons.pdf.aura"]} />} element={<GenderPage productKey={EProductKeys["moons.pdf.aura"]} />}
/> >
<Route path=":targetId" element={<GenderPage />} />
</Route>
</Route> </Route>
<Route <Route
element={ element={
@ -419,7 +427,9 @@ function App(): JSX.Element {
<Route <Route
path={routes.client.advisorChatGender()} path={routes.client.advisorChatGender()}
element={<GenderPage productKey={EProductKeys["chat.aura"]} />} element={<GenderPage productKey={EProductKeys["chat.aura"]} />}
/> >
<Route path=":targetId" element={<GenderPage />} />
</Route>
</Route> </Route>
<Route <Route
element={ element={
@ -590,34 +600,6 @@ function App(): JSX.Element {
</Route> </Route>
{/* Advisor short path */} {/* Advisor short path */}
{/* Single Payment Page Short Path */}
{/* <Route
element={
<ShortPathOutlet
productKey={EProductKeys["chat.aura"]}
redirectUrls={{
data: {
no: routes.client.advisorChatGender(),
force: routes.client.advisorChatBirthdate(),
},
}}
requiredParameters={[
birthdate,
birthPlace,
isForceShortPath || gender,
]}
/>
}
>
<Route
path={routes.client.advisorChatPayment()}
element={<PaymentWithEmailPage />}
>
<Route path=":productId" element={<PaymentWithEmailPage />} />
</Route>
</Route> */}
{/* Single Payment Page Short Path */}
{/* Test Routes Start */} {/* Test Routes Start */}
<Route path={routes.client.notFound()} element={<NotFoundPage />} /> <Route path={routes.client.notFound()} element={<NotFoundPage />} />
<Route path={routes.client.gender()} element={<GenderPage />}> <Route path={routes.client.gender()} element={<GenderPage />}>
@ -805,10 +787,6 @@ function App(): JSX.Element {
/> />
<Route path={routes.client.static()} element={<StaticPage />} /> <Route path={routes.client.static()} element={<StaticPage />} />
<Route path={routes.client.priceList()} element={<PriceListPage />} /> <Route path={routes.client.priceList()} element={<PriceListPage />} />
{/* <Route
path={routes.client.wallpaper()}
element={<ProtectWallpaperPage />}
/> */}
</Route> </Route>
<Route element={<AuthorizedUserOutlet />}> <Route element={<AuthorizedUserOutlet />}>
<Route <Route
@ -820,18 +798,10 @@ function App(): JSX.Element {
</Route> </Route>
<Route element={<PrivateOutlet />}> <Route element={<PrivateOutlet />}>
<Route element={<AuthorizedUserOutlet />}> <Route element={<AuthorizedUserOutlet />}>
{/* <Route
path={routes.client.subscription()}
element={<SubscriptionPage />}
/> */}
<Route <Route
path={routes.client.paymentMethod()} path={routes.client.paymentMethod()}
element={<PaymentPage />} element={<PaymentPage />}
/> />
<Route
path={routes.client.paymentStripe()}
element={<StripePage />}
/>
</Route> </Route>
<Route element={<PrivateSubscriptionOutlet />}> <Route element={<PrivateSubscriptionOutlet />}>
<Route path={routes.client.home()} element={<HomePage />} /> <Route path={routes.client.home()} element={<HomePage />} />

View File

@ -4,7 +4,6 @@ import { useNavigate, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store"; import { actions, selectors } from "@/store";
import { useApi } from "@/api";
import Title from "../Title"; import Title from "../Title";
import Policy from "../Policy"; import Policy from "../Policy";
import EmailInput from "./EmailInput"; import EmailInput from "./EmailInput";
@ -12,9 +11,10 @@ import MainButton from "../MainButton";
import Loader, { LoaderColor } from "../Loader"; import Loader, { LoaderColor } from "../Loader";
import routes from "@/routes"; import routes from "@/routes";
import NameInput from "./NameInput"; import NameInput from "./NameInput";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import { useAuthentication } from "@/hooks/authentication/use-authentication"; import { useAuthentication } from "@/hooks/authentication/use-authentication";
import { ESourceAuthorization } from "@/api/resources/User"; import { ESourceAuthorization } from "@/api/resources/User";
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
import { usePaywall } from "@/hooks/paywall/usePaywall";
interface IEmailEnterPage { interface IEmailEnterPage {
redirectUrl?: string; redirectUrl?: string;
@ -25,8 +25,7 @@ function EmailEnterPage({
redirectUrl = routes.client.emailConfirm(), redirectUrl = routes.client.emailConfirm(),
isRequiredName = false, isRequiredName = false,
}: IEmailEnterPage): JSX.Element { }: IEmailEnterPage): JSX.Element {
const api = useApi(); const { t } = useTranslation();
const { t, i18n } = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
@ -35,51 +34,31 @@ function EmailEnterPage({
const [isValidEmail, setIsValidEmail] = useState(false); const [isValidEmail, setIsValidEmail] = useState(false);
const [isValidName, setIsValidName] = useState(!isRequiredName); const [isValidName, setIsValidName] = useState(!isRequiredName);
const [isAuth, setIsAuth] = useState(false); const [isAuth, setIsAuth] = useState(false);
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]); const activeProductFromStore = useSelector(selectors.selectActiveProduct);
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan);
const [activeSubPlan, setActiveSubPlan] = useState<ISubscriptionPlan | null>(
activeSubPlanFromStore
);
const locale = i18n.language;
const { subPlan } = useParams(); const { subPlan } = useParams();
const { error, isLoading, authorization } = useAuthentication(); const { error, isLoading, authorization } = useAuthentication();
const { products } = usePaywall({
placementKey: EPlacementKeys["aura.placement.main"],
});
const [activeProduct, setActiveProduct] = useState<IPaywallProduct | null>(
activeProductFromStore
);
useEffect(() => { useEffect(() => {
if (subPlan) { if (subPlan) {
const targetSubPlan = subPlans.find( const targetProduct = products.find(
(sub_plan) => (product) =>
String( String(
sub_plan?.trial?.price_cents product?.trialPrice
? Math.floor((sub_plan?.trial?.price_cents + 1) / 100) ? Math.floor((product?.trialPrice + 1) / 100)
: sub_plan.id.replace(".", "") : product.key.replace(".", "")
) === subPlan ) === subPlan
); );
if (targetSubPlan) { if (targetProduct) {
setActiveSubPlan(targetSubPlan); setActiveProduct(targetProduct);
} }
} }
}, [subPlan, subPlans]); }, [subPlan, products]);
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 handleValidEmail = (email: string) => { const handleValidEmail = (email: string) => {
dispatch(actions.form.addEmail(email)); dispatch(actions.form.addEmail(email));
@ -124,7 +103,7 @@ function EmailEnterPage({
await authorization(email, source); await authorization(email, source);
dispatch( dispatch(
actions.payment.update({ actions.payment.update({
activeSubPlan, activeProduct,
}) })
); );
setIsAuth(true); setIsAuth(true);

View File

@ -1,7 +1,6 @@
import { getRandomArbitrary, getRandomName } from "@/services/random-value"; import { getRandomArbitrary, getRandomName } from "@/services/random-value";
import EmailItem, { IEmailItemProps } from "../EmailItem"; import EmailItem, { IEmailItemProps } from "../EmailItem";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { useTranslation } from "react-i18next";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
const getEmails = (): IEmailItemProps[] => { const getEmails = (): IEmailItemProps[] => {
@ -18,6 +17,7 @@ const getEmails = (): IEmailItemProps[] => {
}; };
interface IEmailsListProps { interface IEmailsListProps {
title: string | JSX.Element | JSX.Element[];
classNameContainer?: string; classNameContainer?: string;
classNameTitle?: string; classNameTitle?: string;
classNameEmailItem?: string; classNameEmailItem?: string;
@ -25,25 +25,16 @@ interface IEmailsListProps {
} }
function EmailsList({ function EmailsList({
title,
classNameContainer = "", classNameContainer = "",
classNameTitle = "", classNameTitle = "",
classNameEmailItem = "", classNameEmailItem = "",
direction = "up-down", direction = "up-down",
}: IEmailsListProps): JSX.Element { }: IEmailsListProps): JSX.Element {
const { t } = useTranslation();
const [countUsers, setCountUsers] = useState(752);
const [emails, setEmails] = useState(getEmails()); const [emails, setEmails] = useState(getEmails());
const [elementIdx, setElementIdx] = useState(0); const [elementIdx, setElementIdx] = useState(0);
const itemsRef = useRef<HTMLDivElement[]>([]); const itemsRef = useRef<HTMLDivElement[]>([]);
useEffect(() => {
const randomDelay = getRandomArbitrary(3000, 5000);
const countUsersTimeOut = setTimeout(() => {
setCountUsers((prevState) => prevState + 1);
}, randomDelay);
return () => clearTimeout(countUsersTimeOut);
}, [countUsers]);
useEffect(() => { useEffect(() => {
let randomDelay = getRandomArbitrary(500, 5000); let randomDelay = getRandomArbitrary(500, 5000);
if (!elementIdx) { if (!elementIdx) {
@ -69,11 +60,7 @@ function EmailsList({
return ( return (
<div className={`${styles.container} ${classNameContainer}`}> <div className={`${styles.container} ${classNameContainer}`}>
<span className={`${styles["title"]} ${classNameTitle}`}> <span className={`${styles["title"]} ${classNameTitle}`}>{title}</span>
{t("people_joined_today", {
countPeoples: <strong>{countUsers}</strong>,
})}
</span>
<div className={`${styles["emails-container"]} ${styles[direction]}`}> <div className={`${styles["emails-container"]} ${styles[direction]}`}>
{emails.map(({ email, price }, idx) => ( {emails.map(({ email, price }, idx) => (
<div <div

View File

@ -5,14 +5,14 @@ import {
useElements, useElements,
} from "@stripe/react-stripe-js"; } from "@stripe/react-stripe-js";
import { PaymentRequest } from "@stripe/stripe-js"; import { PaymentRequest } from "@stripe/stripe-js";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import routes from "@/routes"; import routes from "@/routes";
import { IPaywallProduct } from "@/api/resources/Paywall";
interface ApplePayButtonProps { interface ApplePayButtonProps {
activeSubPlan: ISubscriptionPlan | null; activeProduct: IPaywallProduct | null;
client_secret: string; client_secret: string;
subscriptionReceiptId?: string; subscriptionReceiptId?: string;
returnUrl?: string; returnUrl?: string;
@ -20,7 +20,7 @@ interface ApplePayButtonProps {
} }
function ApplePayButton({ function ApplePayButton({
activeSubPlan, activeProduct,
client_secret, client_secret,
subscriptionReceiptId, subscriptionReceiptId,
returnUrl, returnUrl,
@ -34,15 +34,15 @@ function ApplePayButton({
null null
); );
const getAmountFromSubPlan = (subPlan: ISubscriptionPlan) => { const getAmountFromProduct = (subPlan: IPaywallProduct) => {
if (subPlan.trial) { if (subPlan.isTrial) {
return subPlan.trial.price_cents; return subPlan.trialPrice;
} }
return subPlan.price_cents; return subPlan.price;
}; };
useEffect(() => { useEffect(() => {
if (!stripe || !elements || !activeSubPlan) { if (!stripe || !elements || !activeProduct) {
return; return;
} }
@ -50,8 +50,8 @@ function ApplePayButton({
country: "US", country: "US",
currency: "usd", currency: "usd",
total: { total: {
label: activeSubPlan.name || "Subscription", label: activeProduct.name || "Subscription",
amount: getAmountFromSubPlan(activeSubPlan), amount: getAmountFromProduct(activeProduct),
}, },
requestPayerName: true, requestPayerName: true,
requestPayerEmail: true, requestPayerEmail: true,
@ -95,7 +95,6 @@ function ApplePayButton({
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [
activeSubPlan,
client_secret, client_secret,
dispatch, dispatch,
elements, elements,

View File

@ -16,12 +16,14 @@ interface ICheckoutFormProps {
children?: JSX.Element | null; children?: JSX.Element | null;
subscriptionReceiptId?: string; subscriptionReceiptId?: string;
returnUrl?: string; returnUrl?: string;
confirmType?: "payment" | "setup";
} }
export default function CheckoutForm({ export default function CheckoutForm({
children, children,
subscriptionReceiptId, subscriptionReceiptId,
returnUrl, returnUrl,
confirmType = "payment",
}: ICheckoutFormProps) { }: ICheckoutFormProps) {
const stripe = useStripe(); const stripe = useStripe();
const elements = useElements(); const elements = useElements();
@ -42,7 +44,9 @@ export default function CheckoutForm({
setIsProcessing(true); setIsProcessing(true);
try { try {
const { error } = await stripe.confirmPayment({ const { error } = await stripe[
confirmType === "payment" ? "confirmPayment" : "confirmSetup"
]({
elements, elements,
confirmParams: { confirmParams: {
return_url: returnUrl return_url: returnUrl

View File

@ -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 (
<MainButton color='blue' onClick={onClick}>
{t('stripe')}
</MainButton>
)
}

View File

@ -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<Promise<Stripe | null> | null>(null);
const [clientSecret, setClientSecret] = useState<string>("");
const [subscriptionReceiptId, setSubscriptionReceiptId] =
useState<string>("");
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 (
<Modal open={open} onClose={handleClose}>
{isLoading ? (
<div className={styles["payment-loader"]}>
<Loader />
</div>
) : null}
{!isLoading && (
<>
<Title variant="h2" className={styles.title}>
Choose payment method
</Title>
<p className={styles.email}>{email}</p>
</>
)}
{stripePromise && clientSecret && subscriptionReceiptId && (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<ApplePayButton
activeSubPlan={activeSubPlan}
client_secret={clientSecret}
subscriptionReceiptId={subscriptionReceiptId}
/>
{activeSubPlan && (
<SubPlanInformation subPlan={activeSubPlan} />
)}
<CheckoutForm subscriptionReceiptId={subscriptionReceiptId} />
</Elements>
)}
</Modal>
);
}

View File

@ -1,2 +0,0 @@
export * from './Button'
export * from './Modal'

View File

@ -3,89 +3,17 @@ import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { actions } from "@/store"; import { actions } from "@/store";
// import { SubscriptionReceipts, useApi, useApiCall } from "@/api";
// import { useAuth } from "@/auth";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import Loader from "@/components/Loader"; import Loader from "@/components/Loader";
import { paymentResultPathsOfProducts } from "@/data/products"; import { paymentResultPathsOfProducts } from "@/data/products";
function PaymentResultPage(): JSX.Element { function PaymentResultPage(): JSX.Element {
// const api = useApi();
// const { token } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const status = searchParams.get("redirect_status"); const status = searchParams.get("redirect_status");
const redirect_type = searchParams.get("redirect_type"); const redirect_type = searchParams.get("redirect_type");
// const { id } = useParams();
// const requestTimeOutRef = useRef<NodeJS.Timeout>();
const [isLoading] = useState(true); const [isLoading] = useState(true);
// const [subscriptionReceipt, setSubscriptionReceipt] =
// useState<SubscriptionReceipts.SubscriptionReceipt>();
// 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<SubscriptionReceipts.Response | null>(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(() => { useEffect(() => {
if (status === "succeeded") { if (status === "succeeded") {

View File

@ -3,37 +3,39 @@ import PriceItem from "../PriceItem";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { actions } from "@/store"; import { actions } from "@/store";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans"; import { IPaywallProduct } from "@/api/resources/Paywall";
interface PriceListProps { interface PriceListProps {
subPlans: ISubscriptionPlan[]; products: IPaywallProduct[];
activeItem: number | null; activeItem: number | null;
classNameItem?: string; classNameItem?: string;
classNameItemActive?: string; classNameItemActive?: string;
click: () => void; click: () => void;
} }
const getPrice = (plan: ISubscriptionPlan) => { const getPrice = (product: IPaywallProduct) => {
return (plan.trial?.price_cents || 0) / 100; return (product.trialPrice || 0) / 100;
}; };
function PriceList({ function PriceList({
click, click,
subPlans, products,
classNameItem = "", classNameItem = "",
classNameItemActive = "", classNameItemActive = "",
}: PriceListProps): JSX.Element { }: PriceListProps): JSX.Element {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [activePlanItem, setActivePlanItem] = const [activeProduct, setActiveProduct] = useState<IPaywallProduct | null>(
useState<ISubscriptionPlan | null>(null); null
);
const priceItemClick = (id: string) => { const priceItemClick = (id: string) => {
const activePlan = subPlans.find((item) => item.id === String(id)) || null; const activeProduct =
setActivePlanItem(activePlan); products.find((item) => item._id === String(id)) || null;
if (activePlan) { setActiveProduct(activeProduct);
if (activeProduct) {
dispatch( dispatch(
actions.payment.update({ actions.payment.update({
activeSubPlan: activePlan, activeProduct,
}) })
); );
} }
@ -42,12 +44,12 @@ function PriceList({
return ( return (
<div className={`${styles.container}`}> <div className={`${styles.container}`}>
{subPlans.map((plan, idx) => ( {products.map((product, idx) => (
<PriceItem <PriceItem
active={plan.id === activePlanItem?.id} active={product._id === activeProduct?._id}
key={idx} key={idx}
value={getPrice(plan)} value={getPrice(product)}
id={plan.id} id={product._id}
className={classNameItem} className={classNameItem}
classNameActive={classNameItemActive} classNameActive={classNameItemActive}
click={priceItemClick} click={priceItemClick}

View File

@ -8,42 +8,31 @@ import Title from "../Title";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import EmailsList from "../EmailsList"; import EmailsList from "../EmailsList";
import PriceList from "../PriceList"; import PriceList from "../PriceList";
import { useApi } from "@/api";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import Loader from "../Loader"; import Loader from "../Loader";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import { EPlacementKeys } from "@/api/resources/Paywall";
import { getRandomArbitrary } from "@/services/random-value";
function PriceListPage(): JSX.Element { function PriceListPage(): JSX.Element {
const { t, i18n } = useTranslation(); const { t } = useTranslation();
const locale = i18n.language;
const api = useApi();
const navigate = useNavigate(); const navigate = useNavigate();
const dispatch = useDispatch(); const dispatch = useDispatch();
const homeConfig = useSelector(selectors.selectHome); const homeConfig = useSelector(selectors.selectHome);
const selectedPrice = useSelector(selectors.selectSelectedPrice); const selectedPrice = useSelector(selectors.selectSelectedPrice);
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
const email = useSelector(selectors.selectEmail); const email = useSelector(selectors.selectEmail);
const { products, getText } = usePaywall({
placementKey: EPlacementKeys["aura.placement.main"],
});
const [countUsers, setCountUsers] = useState(752);
useEffect(() => { useEffect(() => {
(async () => { const randomDelay = getRandomArbitrary(3000, 5000);
const { sub_plans } = await api.getSubscriptionPlans({ locale }); const countUsersTimeOut = setTimeout(() => {
const plans = sub_plans setCountUsers((prevState) => prevState + 1);
.filter((plan: ISubscriptionPlan) => plan.provider === "stripe") }, randomDelay);
.sort((a, b) => { return () => clearTimeout(countUsersTimeOut);
if (!a.trial || !b.trial) { }, [countUsers]);
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 handleNext = () => { const handleNext = () => {
dispatch( dispatch(
@ -60,25 +49,33 @@ function PriceListPage(): JSX.Element {
<> <>
<UserHeader email={email} /> <UserHeader email={email} />
<section className={`${styles.page} page`}> <section className={`${styles.page} page`}>
{!!subPlans.length && ( {!!products.length && (
<> <>
<Title className={styles.title} variant="h2"> <Title className={styles.title} variant="h2">
{t("choose_your_own_fee")} {t("choose_your_own_fee")}
</Title> </Title>
<p className={styles.slogan}>{t("aura.web.price_selection")}</p> <p className={styles.slogan}>{t("aura.web.price_selection")}</p>
<div className={styles["emails-list-container"]}> <div className={styles["emails-list-container"]}>
<EmailsList /> <EmailsList
title={getText("text.5", {
replacementSelector: "strong",
replacement: {
target: "${quantity}",
replacement: countUsers.toString(),
},
})}
/>
</div> </div>
<div className={styles["price-list-container"]}> <div className={styles["price-list-container"]}>
<PriceList <PriceList
activeItem={selectedPrice} activeItem={selectedPrice}
subPlans={subPlans} products={products}
click={handleNext} click={handleNext}
/> />
</div> </div>
</> </>
)} )}
{!subPlans.length && <Loader />} {!products.length && <Loader />}
</section> </section>
</> </>
); );

View File

@ -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<Promise<Stripe | null> | null>(null);
const [clientSecret, setClientSecret] = useState<string>("");
const [subscriptionReceiptId, setSubscriptionReceiptId] =
useState<string>("");
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 (
<div className={`${styles.page} page`}>
{isLoading ? (
<div className={styles["payment-loader"]}>
<Loader />
</div>
) : null}
{!isLoading && (
<>
<Title variant="h2" className={styles.title}>
Pay
</Title>
<p className={styles.email}>{email}</p>
</>
)}
{stripePromise && clientSecret && subscriptionReceiptId && (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<ApplePayButton
activeSubPlan={activeSubPlan}
client_secret={clientSecret}
subscriptionReceiptId={subscriptionReceiptId}
/>
{activeSubPlan && (
<SubPlanInformation subPlan={activeSubPlan} />
)}
<CheckoutForm subscriptionReceiptId={subscriptionReceiptId} />
</Elements>
)}
</div>
);
}

View File

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

View File

@ -1,34 +1,34 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import TotalToday from "./TotalToday"; import TotalToday from "./TotalToday";
import ApplePayButton from "../StripePage/ApplePayButton"; import ApplePayButton from "../PaymentPage/methods/ApplePayButton";
import { IPaywallProduct } from "@/api/resources/Paywall";
interface ISubPlanInformationProps { interface ISubPlanInformationProps {
subPlan: ISubscriptionPlan; product: IPaywallProduct;
client_secret?: string; client_secret?: string;
} }
const getPrice = (plan: ISubscriptionPlan): string => { const getPrice = (product: IPaywallProduct): string => {
return `$${ return `$${
(plan.trial?.price_cents === 100 ? 99 : plan.trial?.price_cents || 0) / 100 (product.trialPrice === 100 ? 99 : product.trialPrice || 0) / 100
}`; }`;
}; };
function SubPlanInformation({ function SubPlanInformation({
subPlan, product,
client_secret, client_secret,
}: ISubPlanInformationProps): JSX.Element { }: ISubPlanInformationProps): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className={styles.container}> <div className={styles.container}>
<TotalToday total={getPrice(subPlan)} /> <TotalToday total={getPrice(product)} />
{client_secret && ( {client_secret && (
<ApplePayButton activeSubPlan={subPlan} client_secret={client_secret} /> <ApplePayButton activeProduct={product} client_secret={client_secret} />
)} )}
<p className={styles.description}> <p className={styles.description}>
{t("auweb.pay.information").replaceAll("%@", getPrice(subPlan))}. {t("auweb.pay.information").replaceAll("%@", getPrice(product))}.
</p> </p>
</div> </div>
); );

View File

@ -11,22 +11,23 @@ import styles from "./styles.module.css";
// import Header from "../Header"; // import Header from "../Header";
// import SpecialWelcomeOffer from "../SpecialWelcomeOffer"; // import SpecialWelcomeOffer from "../SpecialWelcomeOffer";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ISubscriptionPlan, ITrial } from "@/api/resources/SubscriptionPlans";
import { ApiError, extractErrorMessage, useApi } from "@/api"; import { ApiError, extractErrorMessage, useApi } from "@/api";
import { useAuth } from "@/auth"; import { useAuth } from "@/auth";
import { getClientLocale, getClientTimezone } from "@/locales"; import { getClientLocale, getClientTimezone } from "@/locales";
import Loader from "../Loader"; import Loader from "../Loader";
import Title from "../Title"; import Title from "../Title";
import ErrorText from "../ErrorText"; import ErrorText from "../ErrorText";
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
import { usePaywall } from "@/hooks/paywall/usePaywall";
const currency = Currency.USD; const currency = Currency.USD;
const locale = getClientLocale() as Locale; const locale = getClientLocale() as Locale;
const getPriceFromTrial = (trial: ITrial | null) => { const getPrice = (product: IPaywallProduct | null) => {
if (!trial) { if (!product?.trialPrice) {
return 0; 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 { function SubscriptionPage(): JSX.Element {
@ -48,37 +49,40 @@ function SubscriptionPage(): JSX.Element {
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [apiError, setApiError] = useState<ApiError | null>(null); const [apiError, setApiError] = useState<ApiError | null>(null);
const [error, setError] = useState<boolean>(false); const [error, setError] = useState<boolean>(false);
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan);
const [activeSubPlan, setActiveSubPlan] = useState<ISubscriptionPlan | null>(
activeSubPlanFromStore
);
const { subPlan } = useParams(); const { subPlan } = useParams();
const birthday = useSelector(selectors.selectBirthday); 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<IPaywallProduct | null>(
activeProductFromStore
);
useEffect(() => { useEffect(() => {
if (subPlan) { if (subPlan) {
const targetSubPlan = subPlans.find( const targetProduct = products.find(
(sub_plan) => (product) =>
String( String(
sub_plan?.trial?.price_cents product?.trialPrice
? Math.floor((sub_plan?.trial?.price_cents + 1) / 100) ? Math.floor((product?.trialPrice + 1) / 100)
: sub_plan.id.replace(".", "") : product.key.replace(".", "")
) === subPlan ) === subPlan
); );
if (targetSubPlan) { if (targetProduct) {
setActiveSubPlan(targetSubPlan); setActiveProduct(targetProduct);
} }
} }
}, [subPlan, subPlans]); }, [products, subPlan]);
const paymentItems = [ const paymentItems = [
{ {
title: activeSubPlan?.name || "Per 7-Day Trial For", title: activeProduct?.name || "Per 7-Day Trial For",
price: getPriceFromTrial(activeSubPlan?.trial || null), price: getPrice(activeProduct),
description: activeSubPlan?.desc.length description: activeProduct?.description?.length
? activeSubPlan?.desc ? activeProduct.description
: t("au.2week_plan.web"), : t("au.2week_plan.web"),
}, },
]; ];
@ -111,7 +115,7 @@ function SubscriptionPage(): JSX.Element {
dispatch(actions.status.update("registred")); dispatch(actions.status.update("registred"));
dispatch( dispatch(
actions.payment.update({ actions.payment.update({
activeSubPlan, activeProduct,
}) })
); );
setIsLoading(false); setIsLoading(false);
@ -176,29 +180,6 @@ function SubscriptionPage(): JSX.Element {
setName(name); 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 ( return (
<> <>
{/* <SpecialWelcomeOffer open={isOpenModal} onClose={handleClick} /> */} {/* <SpecialWelcomeOffer open={isOpenModal} onClose={handleClick} /> */}
@ -274,7 +255,7 @@ function SubscriptionPage(): JSX.Element {
</div> </div>
<div className={styles["subscription-action"]}> <div className={styles["subscription-action"]}>
<MainButton onClick={handleClick}> <MainButton onClick={handleClick}>
Start ${getPriceFromTrial(activeSubPlan?.trial || null)} Start ${getPrice(activeProduct || null)}
</MainButton> </MainButton>
</div> </div>
<Policy> <Policy>

View File

@ -2,38 +2,37 @@ import { useState } from "react";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { actions } from "@/store"; import { actions } from "@/store";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import PriceItem from "../PriceItem"; import PriceItem from "../PriceItem";
import { IPaywallProduct } from "@/api/resources/Paywall";
interface PriceListProps { interface PriceListProps {
subPlans: ISubscriptionPlan[]; products: IPaywallProduct[];
activeItem: number | null; activeItem: number | null;
classNameItem?: string; classNameItem?: string;
classNameItemActive?: string; classNameItemActive?: string;
click: () => void; click: () => void;
} }
const getPrice = (plan: ISubscriptionPlan) => { const getPrice = (product: IPaywallProduct) => {
return (plan.trial?.price_cents || 0) / 100; return (product.trialPrice || 0) / 100;
}; };
function PriceList({ function PriceList({
click, click,
subPlans, products,
classNameItem = "", classNameItem = "",
classNameItemActive = "", classNameItemActive = "",
}: PriceListProps): JSX.Element { }: PriceListProps): JSX.Element {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [activePlanItem, setActivePlanItem] = const [activeProductItem, setActiveProductItem] = useState<IPaywallProduct>();
useState<ISubscriptionPlan | null>(null);
const priceItemClick = (id: string) => { const priceItemClick = (id: string) => {
const activePlan = subPlans.find((item) => item.id === String(id)) || null; const activeProduct = products.find((item) => item._id === String(id));
setActivePlanItem(activePlan); setActiveProductItem(activeProduct);
if (activePlan) { if (activeProduct) {
dispatch( dispatch(
actions.payment.update({ actions.payment.update({
activeSubPlan: activePlan, activeProduct,
}) })
); );
} }
@ -42,12 +41,12 @@ function PriceList({
return ( return (
<div className={`${styles.container}`}> <div className={`${styles.container}`}>
{subPlans.map((plan, idx) => ( {products.map((product, idx) => (
<PriceItem <PriceItem
active={plan.id === activePlanItem?.id} active={product._id === activeProductItem?._id}
key={idx} key={idx}
value={getPrice(plan)} value={getPrice(product)}
id={plan.id} id={product._id}
className={classNameItem} className={classNameItem}
classNameActive={classNameItemActive} classNameActive={classNameItemActive}
click={priceItemClick} click={priceItemClick}

View File

@ -15,4 +15,5 @@
.image { .image {
position: absolute; position: absolute;
bottom: 8px; bottom: 8px;
z-index: -1;
} }

View File

@ -1,6 +1,5 @@
import { IAnswer } from "@/data"; import { IAnswer } from "@/data";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store"; import { actions, selectors } from "@/store";
@ -20,34 +19,36 @@ function MultiplyAnswers({ answers }: IMultiplyAnswersProps) {
const { currentlyAffecting, gender } = useSelector( const { currentlyAffecting, gender } = useSelector(
selectors.selectQuestionnaire selectors.selectQuestionnaire
); );
const [selectedAnswers, setSelectedAnswers] = useState<string[]>(
currentlyAffecting?.split("$") || []
);
const handleClick = (answer: IAnswer) => { const handleClick = (answer: IAnswer) => {
if (selectedAnswers.includes(answer.id)) { if (currentlyAffecting.includes(`$${answer.id}`)) {
return setSelectedAnswers((prevState) => return dispatch(
prevState.filter((item) => item !== answer.id) actions.questionnaire.update({
currentlyAffecting: currentlyAffecting.replace(`$${answer.id}`, ""),
})
); );
} }
if (answer.id === "none_of_these") { if (answer.id === "none_of_these") {
return setSelectedAnswers([answer.id]); return dispatch(
actions.questionnaire.update({
currentlyAffecting: `$${answer.id}`,
})
);
} }
if ( if (
selectedAnswers.includes("none_of_these") && currentlyAffecting.includes("$none_of_these") &&
answer.id !== "none_of_these" answer.id !== "none_of_these"
) { ) {
return; return;
} }
return setSelectedAnswers((prevState) => [...prevState, answer.id]); return dispatch(
actions.questionnaire.update({
currentlyAffecting: `${currentlyAffecting}$${answer.id}`,
})
);
}; };
const handleNext = () => { const handleNext = () => {
dispatch(
actions.questionnaire.update({
currentlyAffecting: selectedAnswers.join("$"),
})
);
navigate( navigate(
`${routes.client.questionnaireV1()}/relationships/partnerPriority` `${routes.client.questionnaireV1()}/relationships/partnerPriority`
); );
@ -55,30 +56,35 @@ function MultiplyAnswers({ answers }: IMultiplyAnswersProps) {
return ( return (
<> <>
{answers.map((answer, index) => ( <div className={styles["multiply-answers"]}>
<Answer {answers.map((answer, index) => (
key={index} <Answer
answer={answer} key={index}
disabled={ answer={answer}
selectedAnswers.includes("none_of_these") && disabled={
answer.id !== "none_of_these" currentlyAffecting.includes("$none_of_these") &&
} answer.id !== "none_of_these"
classNameContainer={ }
selectedAnswers.includes(answer.id) ? styles["answer-active"] : "" classNameContainer={
} currentlyAffecting.includes(`${answer.id}`)
type="multiply" ? styles["answer-active"]
active={selectedAnswers.includes(answer.id)} : ""
gender={gender} }
onClick={() => handleClick(answer)} type="multiply"
/> active={currentlyAffecting.includes(`${answer.id}`)}
))} gender={gender}
<QuestionnaireGreenButton onClick={() => handleClick(answer)}
className={styles.button} />
onClick={handleNext} ))}
disabled={!selectedAnswers.length} </div>
> {!!currentlyAffecting.length && (
{t("next")} <QuestionnaireGreenButton
</QuestionnaireGreenButton> className={styles.button}
onClick={handleNext}
>
{t("next")}
</QuestionnaireGreenButton>
)}
</> </>
); );
} }

View File

@ -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 { .button {
position: fixed;
bottom: calc(0dvh + 16px);
width: calc(100% - 64px);
max-width: 396px;
margin-top: 8px; margin-top: 8px;
} }

View File

@ -49,6 +49,7 @@
align-items: center; align-items: center;
gap: 11px; gap: 11px;
margin-top: 28px; margin-top: 28px;
z-index: 0;
} }
.description { .description {

View File

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

View File

@ -4,10 +4,8 @@ import { useNavigate, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store"; import { actions, selectors } from "@/store";
import { useApi } from "@/api";
import routes from "@/routes"; import routes from "@/routes";
import NameInput from "./NameInput"; import NameInput from "./NameInput";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import Title from "@/components/Title"; import Title from "@/components/Title";
import EmailInput from "./EmailInput"; import EmailInput from "./EmailInput";
import Policy from "@/components/Policy"; import Policy from "@/components/Policy";
@ -18,6 +16,8 @@ import { useDynamicSize } from "@/hooks/useDynamicSize";
import QuestionnaireGreenButton from "../../ui/GreenButton"; import QuestionnaireGreenButton from "../../ui/GreenButton";
import { ESourceAuthorization } from "@/api/resources/User"; import { ESourceAuthorization } from "@/api/resources/User";
import { useAuthentication } from "@/hooks/authentication/use-authentication"; import { useAuthentication } from "@/hooks/authentication/use-authentication";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
interface IEmailEnterPage { interface IEmailEnterPage {
redirectUrl?: string; redirectUrl?: string;
@ -28,8 +28,7 @@ function EmailEnterPage({
redirectUrl = routes.client.emailConfirmV1(), redirectUrl = routes.client.emailConfirmV1(),
isRequiredName = false, isRequiredName = false,
}: IEmailEnterPage): JSX.Element { }: IEmailEnterPage): JSX.Element {
const api = useApi(); const { t } = useTranslation();
const { t, i18n } = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
@ -38,53 +37,33 @@ function EmailEnterPage({
const [isValidEmail, setIsValidEmail] = useState(false); const [isValidEmail, setIsValidEmail] = useState(false);
const [isValidName, setIsValidName] = useState(!isRequiredName); const [isValidName, setIsValidName] = useState(!isRequiredName);
const [isAuth, setIsAuth] = useState(false); const [isAuth, setIsAuth] = useState(false);
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan);
const [activeSubPlan, setActiveSubPlan] = useState<ISubscriptionPlan | null>(
activeSubPlanFromStore
);
const locale = i18n.language;
const { subPlan } = useParams(); const { subPlan } = useParams();
const { width: pageWidth, elementRef: pageRef } = useDynamicSize({}); const { width: pageWidth, elementRef: pageRef } = useDynamicSize({});
const { error, isLoading, authorization } = useAuthentication(); const { error, isLoading, authorization } = useAuthentication();
const { gender } = useSelector(selectors.selectQuestionnaire); const { gender } = useSelector(selectors.selectQuestionnaire);
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
const { products } = usePaywall({
placementKey: EPlacementKeys["aura.placement.redesign.main"],
});
const [activeProduct, setActiveProduct] = useState<IPaywallProduct | null>(
activeProductFromStore
);
useEffect(() => { useEffect(() => {
if (subPlan) { if (subPlan) {
const targetSubPlan = subPlans.find( const targetProduct = products.find(
(sub_plan) => (product) =>
String( String(
sub_plan?.trial?.price_cents product?.trialPrice
? Math.floor((sub_plan?.trial?.price_cents + 1) / 100) ? Math.floor((product?.trialPrice + 1) / 100)
: sub_plan.id.replace(".", "") : product.key.replace(".", "")
) === subPlan ) === subPlan
); );
if (targetSubPlan) { if (targetProduct) {
setActiveSubPlan(targetSubPlan); setActiveProduct(targetProduct);
} }
} }
}, [subPlan, subPlans]); }, [subPlan, products]);
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 handleValidEmail = (email: string) => { const handleValidEmail = (email: string) => {
dispatch(actions.form.addEmail(email)); dispatch(actions.form.addEmail(email));
@ -129,7 +108,7 @@ function EmailEnterPage({
await authorization(email, source); await authorization(email, source);
dispatch( dispatch(
actions.payment.update({ actions.payment.update({
activeSubPlan, activeProduct,
}) })
); );
setIsAuth(true); setIsAuth(true);

View File

@ -5,6 +5,8 @@ import routes from "@/routes";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { onboardingTitles } from "../../data/onboarding"; import { onboardingTitles } from "../../data/onboarding";
import ProgressBarLine from "@/components/ui/ProgressBarLine"; import ProgressBarLine from "@/components/ui/ProgressBarLine";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import { EPlacementKeys } from "@/api/resources/Paywall";
function OnboardingPage() { function OnboardingPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -14,6 +16,7 @@ function OnboardingPage() {
const classNameTimeOut = useRef<NodeJS.Timeout>(); const classNameTimeOut = useRef<NodeJS.Timeout>();
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const progressInterval = useRef<NodeJS.Timeout>(); const progressInterval = useRef<NodeJS.Timeout>();
usePaywall({ placementKey: EPlacementKeys["aura.placement.redesign.main"] });
const handleNext = useCallback(() => { const handleNext = useCallback(() => {
navigate(routes.client.trialChoiceV1()); navigate(routes.client.trialChoiceV1());
@ -51,7 +54,11 @@ function OnboardingPage() {
return ( return (
<section className={`${styles.page} page`}> <section className={`${styles.page} page`}>
<img className={styles.image} src="/leo.png" alt="Leo" /> <img
className={`${styles.image} ${styles[periodClassName]}`}
src="/leo.png"
alt="Leo"
/>
{onboardingTitles[activeIndexTitle] && ( {onboardingTitles[activeIndexTitle] && (
<Title className={`${styles.title} ${styles[periodClassName]}`}> <Title className={`${styles.title} ${styles[periodClassName]}`}>
{onboardingTitles[activeIndexTitle]} {onboardingTitles[activeIndexTitle]}

View File

@ -16,6 +16,7 @@
margin-top: 100px; margin-top: 100px;
width: 100%; width: 100%;
max-width: 273px; max-width: 273px;
transition: opacity 1s;
} }
.title { .title {

View File

@ -61,12 +61,12 @@ function RelationshipZodiacInfoPage() {
/> />
<div className={styles["image-container"]}> <div className={styles["image-container"]}>
<img <img
src={`/questionnaire/zodiacs/${gender}/${zodiacSign?.toLowerCase()}.webp`} src={`/questionnaire-redesign/zodiacs/${gender}/pdf.sex.${zodiacSign?.toUpperCase()}.${gender.toUpperCase()}.webp`}
alt="The zodiac signs" alt="The zodiac signs"
/> />
<img src="/plus.svg" alt="Plus" /> <img src="/plus.svg" alt="Plus" />
<img <img
src={`/questionnaire/zodiacs/${partnerGender}/${partnerZodiacSign?.toLowerCase()}.webp`} src={`/questionnaire-redesign/zodiacs/${partnerGender}/pdf.sex.${partnerZodiacSign?.toUpperCase()}.${partnerGender.toUpperCase()}.webp`}
alt="The zodiac signs" alt="The zodiac signs"
/> />
</div> </div>

View File

@ -20,15 +20,17 @@
} }
.image-container { .image-container {
display: flex; display: grid;
justify-content: center;
align-items: center; align-items: center;
justify-items: center;
grid-template-columns: calc(50% - 30px) min-content calc(50% - 30px);
width: 100%; width: 100%;
gap: 32px; gap: 6px;
} }
.image-container > img { .image-container > img {
max-width: 118px; /* max-width: 118px; */
max-height: 196px;
} }
.compatibility-description-container { .compatibility-description-container {

View File

@ -4,17 +4,18 @@ import { selectors } from "@/store";
import { getZodiacSignByDate } from "@/services/zodiac-sign"; import { getZodiacSignByDate } from "@/services/zodiac-sign";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import routes from "@/routes"; import routes from "@/routes";
import { IZodicSignsInfo, zodicSignsInfo } from "@/data"; import { IZodicSignsInfo } from "@/data";
import Title from "@/components/Title"; import Title from "@/components/Title";
import Header from "../../components/Header"; import Header from "../../components/Header";
import QuestionnaireGreenButton from "../../ui/GreenButton"; import QuestionnaireGreenButton from "../../ui/GreenButton";
import { zodiacSignsInfo } from "../../data/zodiacSignsInfo";
function SingleZodiacInfoPage() { function SingleZodiacInfoPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const birthdate = useSelector(selectors.selectQuestionnaire).birthdate; const birthdate = useSelector(selectors.selectQuestionnaire).birthdate;
const gender = useSelector(selectors.selectQuestionnaire).gender; const gender = useSelector(selectors.selectQuestionnaire).gender;
const zodiac = getZodiacSignByDate(birthdate); const zodiac = getZodiacSignByDate(birthdate);
const zodiacInfo = zodicSignsInfo[gender as keyof IZodicSignsInfo].find( const zodiacInfo = zodiacSignsInfo[gender as keyof IZodicSignsInfo].find(
(sign) => sign.name === zodiac (sign) => sign.name === zodiac
); );

View File

@ -1,8 +1,5 @@
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useState } from "react";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import { useTranslation } from "react-i18next";
import { useApi } from "@/api";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store"; import { actions, selectors } from "@/store";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@ -13,66 +10,32 @@ import BackgroundTopBlob from "../../ui/BackgroundTopBlob";
import { useDynamicSize } from "@/hooks/useDynamicSize"; import { useDynamicSize } from "@/hooks/useDynamicSize";
import PriceList from "../../components/PriceList"; import PriceList from "../../components/PriceList";
import QuestionnaireGreenButton from "../../ui/GreenButton"; import QuestionnaireGreenButton from "../../ui/GreenButton";
import { usePaywall } from "@/hooks/paywall/usePaywall";
interface IPlanKey { import { EPlacementKeys } from "@/api/resources/Paywall";
[key: string]: number; import { getRandomArbitrary } from "@/services/random-value";
} import Loader from "@/components/Loader";
function TrialChoicePage() { function TrialChoicePage() {
const { i18n } = useTranslation();
const locale = i18n.language;
const api = useApi();
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const selectedPrice = useSelector(selectors.selectSelectedPrice); const selectedPrice = useSelector(selectors.selectSelectedPrice);
const homeConfig = useSelector(selectors.selectHome); const homeConfig = useSelector(selectors.selectHome);
const email = useSelector(selectors.selectEmail); const email = useSelector(selectors.selectEmail);
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
const [isDisabled, setIsDisabled] = useState(true); const [isDisabled, setIsDisabled] = useState(true);
const allowedPlans = useMemo(() => [""], []); const [countUsers, setCountUsers] = useState(752);
const { width: pageWidth, elementRef: pageRef } = useDynamicSize({}); const { width: pageWidth, elementRef: pageRef } = useDynamicSize({});
const { gender } = useSelector(selectors.selectQuestionnaire); const { gender } = useSelector(selectors.selectQuestionnaire);
const { products, isLoading, getText } = usePaywall({
placementKey: EPlacementKeys["aura.placement.redesign.main"],
});
useEffect(() => { useEffect(() => {
(async () => { const randomDelay = getRandomArbitrary(3000, 5000);
const { sub_plans } = await api.getSubscriptionPlans({ locale }); const countUsersTimeOut = setTimeout(() => {
const plansWithoutTest = sub_plans.filter( setCountUsers((prevState) => prevState + 1);
(plan: ISubscriptionPlan) => !plan.name.includes("(test)") }, randomDelay);
); return () => clearTimeout(countUsersTimeOut);
const plansKeys: IPlanKey = {}; }, [countUsers]);
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 handlePriceItem = () => { const handlePriceItem = () => {
setIsDisabled(false); setIsDisabled(false);
@ -101,60 +64,76 @@ function TrialChoicePage() {
height={180} height={180}
/> />
<Header className={styles.header} /> <Header className={styles.header} />
<p className={styles.text} style={{ marginTop: "60px" }}> {!isLoading && (
We've helped{" "} <>
<span className={styles.blue}> <p className={styles.text} style={{ marginTop: "60px" }}>
<b>millions</b> {getText("text.0", {
</span>{" "} replacementSelector: "b",
of people to have happier lives and better relationships, and we want to color: "#1C38EA",
help you too. })}
</p> </p>
<p className={`${styles.text} ${styles.bold}`}> <p className={`${styles.text} ${styles.bold}`}>
Money shouldnt stand in the way of finding astrology guidance that {getText("text.1", {
finally works. So, choose an amount that you think is reasonable to try color: "#1C38EA",
us out for one week. })}
</p> </p>
<p className={`${styles.text} ${styles.bold} ${styles.blue}`}> <p className={`${styles.text} ${styles.bold} ${styles.blue}`}>
It costs us $13.67 to offer a 3-day trial, but please choose the amount {getText("text.2", {
you are comfortable with. color: "#1C38EA",
</p> })}
<div className={styles["price-container"]}> </p>
<PriceList <div className={styles["price-container"]}>
subPlans={subPlans} <PriceList
activeItem={selectedPrice} products={products}
classNameItem={styles["price-item"]} activeItem={selectedPrice}
classNameItemActive={`${styles["price-item-active"]} ${styles[gender]}`} classNameItem={styles["price-item"]}
click={handlePriceItem} 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 <p className={styles["auxiliary-text"]} style={{ maxWidth: "75%" }}>
trial prices! {getText("text.3", {
</p> color: "#1C38EA",
<img })}
className={styles["arrow-image"]} </p>
src="/arrow.svg" <img
alt={`Arrow to $${subPlans.at(-1)}`} className={styles["arrow-image"]}
/> src="/arrow.svg"
</div> alt={`Arrow to $${products.at(-1)?.trialPrice}`}
<div className={styles["emails-list-container"]}> />
<EmailsList </div>
classNameContainer={`${styles["emails-container"]} ${styles[gender]}`} <div className={styles["emails-list-container"]}>
classNameTitle={styles["emails-title"]} <EmailsList
classNameEmailItem={styles["email-item"]} title={getText("text.5", {
direction="right-left" replacementSelector: "strong",
/> replacement: {
</div> target: "${quantity}",
<p className={styles.email}>{email}</p> replacement: countUsers.toString(),
<QuestionnaireGreenButton },
className={styles.button} })}
disabled={isDisabled} classNameContainer={`${styles["emails-container"]} ${styles[gender]}`}
onClick={handleNext} classNameTitle={styles["emails-title"]}
> classNameEmailItem={styles["email-item"]}
See my plan direction="right-left"
</QuestionnaireGreenButton> />
<p className={styles["auxiliary-text"]}> </div>
*Cost of trial as of February 2024 <p className={styles.email}>{email}</p>
</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> </section>
); );
} }

View File

@ -137,4 +137,11 @@
.email { .email {
font-weight: 500; font-weight: 500;
}
.loader {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
} }

View File

@ -1,6 +1,8 @@
import { useSelector } from "react-redux";
import CustomButton from "../CustomButton"; import CustomButton from "../CustomButton";
import DiscountExpires from "../DiscountExpires"; import DiscountExpires from "../DiscountExpires";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { selectors } from "@/store";
interface IHeaderProps { interface IHeaderProps {
buttonText?: string; buttonText?: string;
@ -13,8 +15,14 @@ function Header({
buttonText = "get my reading", buttonText = "get my reading",
buttonClassName = "", buttonClassName = "",
}: IHeaderProps) { }: IHeaderProps) {
const { gender } = useSelector(selectors.selectQuestionnaire);
return ( return (
<header className={styles.header}> <header
className={styles.header}
style={{
backgroundColor: gender === "male" ? "#C1E5FF" : "#F7EBFF",
}}
>
<DiscountExpires /> <DiscountExpires />
<CustomButton <CustomButton
className={`${styles.button} ${buttonClassName}`} className={`${styles.button} ${buttonClassName}`}

View File

@ -1,5 +1,7 @@
.header { .header {
position: relative; position: sticky;
top: 0;
z-index: 30;
height: 62px; height: 62px;
width: 100%; width: 100%;
max-width: 560px; max-width: 560px;

View File

@ -4,53 +4,61 @@ import PaymentMethodsChoice from "../PaymentMethodsChoice";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods"; import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods";
import { Elements } from "@stripe/react-stripe-js"; import { Elements } from "@stripe/react-stripe-js";
import ApplePayButton from "@/components/StripePage/ApplePayButton"; import ApplePayButton from "@/components/PaymentPage/methods/ApplePayButton";
import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm"; import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm";
import { Stripe, loadStripe } from "@stripe/stripe-js"; import { Stripe, loadStripe } from "@stripe/stripe-js";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { selectors } from "@/store"; import { selectors } from "@/store";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import { useApi } from "@/api";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import routes from "@/routes"; import routes from "@/routes";
import { useTranslation } from "react-i18next";
import { useAuth } from "@/auth";
import Loader from "@/components/Loader"; import Loader from "@/components/Loader";
import { getPriceFromTrial } from "@/services/price";
import SecurityPayments from "../SecurityPayments"; 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 { interface IPaymentModalProps {
activeSubscriptionPlan?: ISubscriptionPlan; activeProduct?: IPaywallProduct;
noTrial?: boolean; noTrial?: boolean;
returnUrl?: string; returnUrl?: string;
placementKey?: EPlacementKeys;
} }
const getPrice = (product: IPaywallProduct) => {
return (product.trialPrice || 0) / 100;
};
function PaymentModal({ function PaymentModal({
activeSubscriptionPlan, activeProduct,
noTrial, noTrial,
returnUrl, returnUrl,
placementKey = EPlacementKeys["aura.placement.redesign.main"],
}: IPaymentModalProps) { }: IPaymentModalProps) {
const { i18n } = useTranslation();
const locale = i18n.language;
const api = useApi();
const { token } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan);
const activeSubPlan = activeSubscriptionPlan
? activeSubscriptionPlan
: activeSubPlanFromStore;
const [stripePromise, setStripePromise] = const [stripePromise, setStripePromise] =
useState<Promise<Stripe | null> | null>(null); useState<Promise<Stripe | null> | null>(null);
const [clientSecret, setClientSecret] = useState<string>("");
const [subscriptionReceiptId, setSubscriptionReceiptId] = const activeProductFromStore = useSelector(selectors.selectActiveProduct);
useState<string>(""); const _activeProduct = activeProduct ? activeProduct : activeProductFromStore;
const [isLoading, setIsLoading] = useState(true); const {
const [isError, setIsError] = useState<boolean>(false); paymentIntentId,
clientSecret,
returnUrl: checkoutUrl,
paymentType,
publicKey,
isLoading,
error,
} = useMakePayment({
productId: _activeProduct?._id || "",
returnPaidUrl:
returnUrl
});
if (checkoutUrl?.length) {
window.location.href = checkoutUrl;
}
const paymentMethodsButtons = useMemo(() => { const paymentMethodsButtons = useMemo(() => {
// return paymentMethods.filter(
// (method) => method.id !== EPaymentMethod.PAYMENT_BUTTONS
// );
return paymentMethods; return paymentMethods;
}, []); }, []);
@ -58,49 +66,24 @@ function PaymentModal({
EPaymentMethod.PAYMENT_BUTTONS EPaymentMethod.PAYMENT_BUTTONS
); );
const { products } = usePaywall({ placementKey });
const onSelectPaymentMethod = (method: EPaymentMethod) => { const onSelectPaymentMethod = (method: EPaymentMethod) => {
setSelectedPaymentMethod(method); setSelectedPaymentMethod(method);
}; };
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const siteConfig = await api.getAppConfig({ bundleId: "auraweb" }); if (!products?.length || !publicKey) return;
setStripePromise(loadStripe(siteConfig.data.stripe_public_key)); setStripePromise(loadStripe(publicKey));
const { sub_plans } = await api.getSubscriptionPlans({ locale }); const isActiveProduct = products.find(
const isActiveSubPlan = sub_plans.find( (product) => product._id === _activeProduct?._id
(subPlan) => subPlan.id === activeSubPlan?.id
); );
if (!activeSubPlan || !isActiveSubPlan) { if (!_activeProduct || !isActiveProduct) {
navigate(routes.client.priceList()); navigate(routes.client.trialChoiceV1());
} }
})(); })();
}, [activeSubPlan, api, locale, navigate]); }, [_activeProduct, navigate, products, publicKey]);
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]);
if (isLoading) { if (isLoading) {
return ( return (
@ -112,7 +95,7 @@ function PaymentModal({
); );
} }
if (isError) { if (error?.length) {
return ( return (
<div className={styles["payment-modal"]}> <div className={styles["payment-modal"]}>
<Title variant="h3" className={styles.title}> <Title variant="h3" className={styles.title}>
@ -132,16 +115,13 @@ function PaymentModal({
selectedPaymentMethod={selectedPaymentMethod} selectedPaymentMethod={selectedPaymentMethod}
onSelectPaymentMethod={onSelectPaymentMethod} onSelectPaymentMethod={onSelectPaymentMethod}
/> />
{activeSubPlan && ( {_activeProduct && (
<div> <div>
{!noTrial && ( {!noTrial && (
<> <>
<p className={styles["sub-plan-description"]}> <p className={styles["sub-plan-description"]}>
You will be charged only{" "} You will be charged only{" "}
<b> <b>${getPrice(_activeProduct)} for your 3-day trial.</b>
${getPriceFromTrial(activeSubPlan?.trial)} for your 3-day
trial.
</b>
</p> </p>
<p className={styles["sub-plan-description"]}> <p className={styles["sub-plan-description"]}>
We`ll <b>email you a reminder</b> before your trial period ends. We`ll <b>email you a reminder</b> before your trial period ends.
@ -160,16 +140,17 @@ function PaymentModal({
{selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && ( {selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && (
<div className={styles["payment-method"]}> <div className={styles["payment-method"]}>
<ApplePayButton <ApplePayButton
activeSubPlan={activeSubPlan} activeProduct={_activeProduct}
client_secret={clientSecret} client_secret={clientSecret}
subscriptionReceiptId={subscriptionReceiptId} subscriptionReceiptId={paymentIntentId}
/> />
</div> </div>
)} )}
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && ( {selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
<CheckoutForm <CheckoutForm
subscriptionReceiptId={subscriptionReceiptId} confirmType={paymentType}
subscriptionReceiptId={paymentIntentId}
returnUrl={returnUrl} returnUrl={returnUrl}
/> />
)} )}

View File

@ -1,19 +1,22 @@
import Title from "@/components/Title"; import Title from "@/components/Title";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { getPriceFromTrial } from "@/services/price";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import CustomButton from "../CustomButton"; import CustomButton from "../CustomButton";
import GuardPayments from "../GuardPayments"; import GuardPayments from "../GuardPayments";
import { useState } from "react"; import { useState } from "react";
import FullScreenModal from "@/components/FullScreenModal"; import FullScreenModal from "@/components/FullScreenModal";
import { IPaywallProduct } from "@/api/resources/Paywall";
interface IPaymentTableProps { interface IPaymentTableProps {
subPlan: ISubscriptionPlan; product: IPaywallProduct;
gender: string; gender: string;
buttonClick: () => void; 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 [isOpenPrivacyModal, setIsOpenPrivacyModal] = useState<boolean>(false);
const handleSubscriptionPolicyClick = (event: React.MouseEvent) => { const handleSubscriptionPolicyClick = (event: React.MouseEvent) => {
event.preventDefault(); event.preventDefault();
@ -50,20 +53,18 @@ function PaymentTable({ gender, subPlan, buttonClick }: IPaymentTableProps) {
<div className={styles["table-container"]}> <div className={styles["table-container"]}>
<Title variant="h3" className={styles.title}> <Title variant="h3" className={styles.title}>
Personalized reading for{" "} Personalized reading for{" "}
<span className={styles.purple}> <span className={styles.purple}>${getPrice(product)}</span>
${getPriceFromTrial(subPlan?.trial)}
</span>
</Title> </Title>
<div className={styles["table-element"]}> <div className={styles["table-element"]}>
<p className={styles["total-today"]}>Total today:</p> <p className={styles["total-today"]}>Total today:</p>
<span>${getPriceFromTrial(subPlan?.trial)}</span> <span>${getPrice(product)}</span>
</div> </div>
<hr /> <hr />
<div className={styles["table-element"]}> <div className={styles["table-element"]}>
<p>Your cost per 2 weeks after trial</p> <p>Your cost per 2 weeks after trial</p>
<div> <div>
<span className={styles.discount}>$65</span> <span className={styles.discount}>$65</span>
<span>${subPlan.price_cents / 100}</span> <span>${product.trialPrice / 100}</span>
</div> </div>
</div> </div>
</div> </div>
@ -75,9 +76,9 @@ function PaymentTable({ gender, subPlan, buttonClick }: IPaymentTableProps) {
<p className={styles.policy}> <p className={styles.policy}>
You are enrolling in 2 weeks subscription. By continuing you agree that 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 $ if you don't cancel prior to the end of the 3-day trial for the $
{getPriceFromTrial(subPlan?.trial)} you will automatically be charged {getPrice(product)} you will automatically be charged $19 every 2 weeks
$19 every 2 weeks until you cancel in settings. Learn more about until you cancel in settings. Learn more about cancellation and refund
cancellation and refund policy in{" "} policy in{" "}
<a onClick={handleSubscriptionPolicyClick}>Subscription policy</a> <a onClick={handleSubscriptionPolicyClick}>Subscription policy</a>
</p> </p>
</> </>

View File

@ -22,7 +22,7 @@ function PersonalInformation({
> >
<div className={styles["image-container"]}> <div className={styles["image-container"]}>
<img <img
src={`/questionnaire/zodiacs/${gender}/${zodiacSign?.toLowerCase()}.webp`} src={`/questionnaire-redesign/zodiacs/${gender}/pdf.sex.${zodiacSign?.toUpperCase()}.${gender.toUpperCase()}.webp`}
alt={`${gender} ${zodiacSign}`} alt={`${gender} ${zodiacSign}`}
/> />
</div> </div>

View File

@ -33,14 +33,15 @@ function WithPartnerInformation(props: IWithPartnerInformationProps) {
<div className={styles["images-container"]}> <div className={styles["images-container"]}>
<div className={styles["image-container"]}> <div className={styles["image-container"]}>
<img <img
src={`/questionnaire/zodiacs/${gender}/${zodiacSign?.toLowerCase()}.webp`} src={`/questionnaire-redesign/zodiacs/${gender}/pdf.sex.${zodiacSign?.toUpperCase()}.${gender.toUpperCase()}.webp`}
alt={`${gender} ${zodiacSign}`} alt={`${gender} ${zodiacSign}`}
/> />
<p>You</p> <p>You</p>
</div> </div>
<img src="/plus.svg" alt="Plus" />
<div className={styles["image-container"]}> <div className={styles["image-container"]}>
<img <img
src={`/questionnaire/zodiacs/${partnerGender}/${partnerZodiacSign?.toLowerCase()}.webp`} src={`/questionnaire-redesign/zodiacs/${partnerGender}/pdf.sex.${partnerZodiacSign?.toUpperCase()}.${partnerGender.toUpperCase()}.webp`}
alt={`${partnerGender} ${partnerZodiacSign}`} alt={`${partnerGender} ${partnerZodiacSign}`}
/> />
<p>Partner</p> <p>Partner</p>

View File

@ -17,8 +17,10 @@
min-height: 100px; min-height: 100px;
border-top-left-radius: 15px; border-top-left-radius: 15px;
border-top-right-radius: 15px; border-top-right-radius: 15px;
display: flex; display: grid;
justify-content: space-around; grid-template-columns: calc(50% - 28px) min-content calc(50% - 28px);
align-items: center;
gap: 4px;
padding-top: 10px; padding-top: 10px;
padding-bottom: 6px; padding-bottom: 6px;
} }
@ -33,6 +35,8 @@
.image-container > img { .image-container > img {
height: 196px; height: 196px;
width: 100%;
object-fit: contain;
} }
.image-container > p { .image-container > p {

View File

@ -40,7 +40,7 @@ function YourReading({
</Title> </Title>
<div className={styles["image-container"]}> <div className={styles["image-container"]}>
<img <img
src={`/questionnaire/zodiacs/${gender}/${zodiacSign?.toLowerCase()}.webp`} src={`/questionnaire-redesign/zodiacs/${gender}/pdf.sex.${zodiacSign?.toUpperCase()}.${gender.toUpperCase()}.webp`}
alt={`${gender} ${zodiacSign}`} alt={`${gender} ${zodiacSign}`}
/> />
</div> </div>

View File

@ -12,11 +12,7 @@ import YourReading from "./components/YourReading";
import Reviews from "./components/Reviews"; import Reviews from "./components/Reviews";
import PointsList from "./components/PointsList"; import PointsList from "./components/PointsList";
import OftenAsk from "./components/OftenAsk"; import OftenAsk from "./components/OftenAsk";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useApi } from "@/api";
import { getClientLocale } from "@/locales";
import { Locale } from "@/components/PaymentTable";
import WithPartnerInformation from "./components/WithPartnerInformation"; import WithPartnerInformation from "./components/WithPartnerInformation";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import PaymentModal from "./components/PaymentModal"; import PaymentModal from "./components/PaymentModal";
@ -26,12 +22,11 @@ import TrialPaymentHeader from "./components/Header";
import Header from "../../components/Header"; import Header from "../../components/Header";
import BackgroundTopBlob from "../../ui/BackgroundTopBlob"; import BackgroundTopBlob from "../../ui/BackgroundTopBlob";
import { useDynamicSize } from "@/hooks/useDynamicSize"; import { useDynamicSize } from "@/hooks/useDynamicSize";
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
const locale = getClientLocale() as Locale; import { usePaywall } from "@/hooks/paywall/usePaywall";
function TrialPaymentPage() { function TrialPaymentPage() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const api = useApi();
const navigate = useNavigate(); const navigate = useNavigate();
const birthdate = useSelector(selectors.selectBirthdate); const birthdate = useSelector(selectors.selectBirthdate);
const zodiacSign = getZodiacSignByDate(birthdate); const zodiacSign = getZodiacSignByDate(birthdate);
@ -46,10 +41,12 @@ function TrialPaymentPage() {
flowChoice, flowChoice,
} = useSelector(selectors.selectQuestionnaire); } = useSelector(selectors.selectQuestionnaire);
const partnerZodiacSign = getZodiacSignByDate(partnerBirthdate); const partnerZodiacSign = getZodiacSignByDate(partnerBirthdate);
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]); const { products } = usePaywall({
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan); placementKey: EPlacementKeys["aura.placement.redesign.main"],
const [activeSubPlan, setActiveSubPlan] = useState<ISubscriptionPlan | null>( });
activeSubPlanFromStore const activeProductFromStore = useSelector(selectors.selectActiveProduct);
const [activeProduct, setActiveProduct] = useState<IPaywallProduct | null>(
activeProductFromStore
); );
const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false); const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false);
const [singleOrWithPartner, setSingleOrWithPartner] = useState< const [singleOrWithPartner, setSingleOrWithPartner] = useState<
@ -57,43 +54,32 @@ function TrialPaymentPage() {
>("single"); >("single");
const { subPlan } = useParams(); 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(() => { useEffect(() => {
if (subPlan) { if (subPlan) {
const targetSubPlan = subPlans.find( const targetProduct = products.find(
(sub_plan) => (product) =>
String( String(
sub_plan?.trial?.price_cents product?.trialPrice
? Math.floor((sub_plan?.trial?.price_cents + 1) / 100) ? Math.floor((product?.trialPrice + 1) / 100)
: sub_plan.id.replace(".", "") : product.key.replace(".", "")
) === subPlan ) === subPlan
); );
if (targetSubPlan) { if (targetProduct) {
setActiveSubPlan(targetSubPlan); setActiveProduct(targetProduct);
dispatch(actions.payment.update({ activeSubPlan: targetSubPlan })); 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(() => { useEffect(() => {
if (["relationship", "married"].includes(flowChoice)) { if (["relationship", "married"].includes(flowChoice)) {
@ -103,12 +89,12 @@ function TrialPaymentPage() {
setSingleOrWithPartner("single"); setSingleOrWithPartner("single");
}, [flowChoice]); }, [flowChoice]);
if (!activeSubPlan) { if (!activeProduct) {
return <Navigate to={routes.client.trialChoice()} />; return <Navigate to={routes.client.trialChoiceV1()} />;
} }
if (!birthdate || !gender || !birthPlace) { if (!birthdate || !gender || !birthPlace) {
return <Navigate to={routes.client.gender()} />; return <Navigate to={routes.client.genderV1()} />;
} }
const handleDiscount = () => { const handleDiscount = () => {
@ -168,7 +154,7 @@ function TrialPaymentPage() {
<Goal goal={goal} /> <Goal goal={goal} />
<PaymentTable <PaymentTable
gender={gender} gender={gender}
subPlan={activeSubPlan} product={activeProduct}
buttonClick={openStripeModal} buttonClick={openStripeModal}
/> />
<YourReading <YourReading
@ -185,7 +171,7 @@ function TrialPaymentPage() {
<OftenAsk /> <OftenAsk />
<PaymentTable <PaymentTable
gender={gender} gender={gender}
subPlan={activeSubPlan} product={activeProduct}
buttonClick={openStripeModal} buttonClick={openStripeModal}
/> />
</section> </section>

View File

@ -6,6 +6,8 @@
padding-bottom: 62px; padding-bottom: 62px;
width: 100%; width: 100%;
max-width: 460px; max-width: 460px;
overflow: inherit;
overflow-x: clip;
} }
.title { .title {

View File

@ -2,10 +2,17 @@ import Title from "@/components/Title";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { selectors } from "@/store"; 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() { function PaymentDiscountTable() {
const activeSub = useSelector(selectors.selectActiveSubPlan); const activeProduct = useSelector(selectors.selectActiveProduct);
return ( return (
<div className={styles.container}> <div className={styles.container}>
@ -15,7 +22,11 @@ function PaymentDiscountTable() {
<p className={styles["no-pressure"]}>No pressure. Cancel anytime.</p> <p className={styles["no-pressure"]}>No pressure. Cancel anytime.</p>
<div className={styles.applied}> <div className={styles.applied}>
<div className={styles.side}> <div className={styles.side}>
<img className={styles["present-image"]} src="/present.png" alt="Present" /> <img
className={styles["present-image"]}
src="/present.png"
alt="Present"
/>
<p className={styles.description}>Secret discount applied!</p> <p className={styles.description}>Secret discount applied!</p>
</div> </div>
<div className={styles.side}> <div className={styles.side}>
@ -27,14 +38,14 @@ function PaymentDiscountTable() {
<p>Your cost per 14 days after trial:</p> <p>Your cost per 14 days after trial:</p>
<div className={styles.side}> <div className={styles.side}>
<span className={styles.discount}>$19</span> <span className={styles.discount}>$19</span>
<strong>$9</strong> <strong>${(activeProduct?.price || 0) / 100}</strong>
</div> </div>
</div> </div>
<p className={styles.save}>You save $30</p> <p className={styles.save}>You save $30</p>
<hr className={styles.line} /> <hr className={styles.line} />
<div className={styles["total-container"]}> <div className={styles["total-container"]}>
<p>Total today:</p> <p>Total today:</p>
{activeSub && <strong>${getPriceFromTrial(activeSub.trial)}</strong>} {activeProduct && <strong>${getPrice(activeProduct)}</strong>}
</div> </div>
</div> </div>
); );

View File

@ -4,19 +4,44 @@ import MainButton from "@/components/MainButton";
import PaymentDiscountTable from "./PaymentDiscountTable"; import PaymentDiscountTable from "./PaymentDiscountTable";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import PaymentModal from "../TrialPayment/components/PaymentModal"; 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() { function TrialPaymentWithDiscount() {
const dispatch = useDispatch();
const { products } = usePaywall({
placementKey: EPlacementKeys["aura.placement.secret.discount"],
});
const productFromStore = useSelector(selectors.selectActiveProduct);
const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false); const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false);
const handleClose = () => { const handleClose = () => {
setIsOpenPaymentModal(false); 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 ( return (
<section className={`${styles.page} page`}> <section className={`${styles.page} page`}>
<Modal open={isOpenPaymentModal} onClose={handleClose}> <Modal open={isOpenPaymentModal} onClose={handleClose}>
<PaymentModal /> <PaymentModal
placementKey={EPlacementKeys["aura.placement.secret.discount"]}
/>
</Modal> </Modal>
<img <img
className={styles["party-popper"]} className={styles["party-popper"]}
@ -35,10 +60,12 @@ function TrialPaymentWithDiscount() {
</MainButton> </MainButton>
<p className={styles.policy}> <p className={styles.policy}>
By continuing you agree that if you don't cancel prior to the end of the 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 3-days trial, you will automatically be charged $
period of 14 days thereafter the standard rate of $9 every 14 days until {(productFromStore?.price || 0) / 100} for the introductory period of 14
you cancel in settings. Learn more about cancellation and refund policy days thereafter the standard rate of $
in Subscription terms. {(productFromStore?.price || 0) / 100} every 14 days until you cancel in
settings. Learn more about cancellation and refund policy in
Subscription terms.
</p> </p>
</section> </section>
); );

View File

@ -10,12 +10,10 @@ import { SinglePayment, useApi, useApiCall } from "@/api";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { selectors } from "@/store"; import { selectors } from "@/store";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { import { ResponsePost } from "@/api/resources/SinglePayment";
ResponsePost,
} from "@/api/resources/SinglePayment";
import { createSinglePayment } from "@/services/singlePayment"; import { createSinglePayment } from "@/services/singlePayment";
import Modal from "@/components/Modal"; 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 { getPriceCentsToDollars } from "@/services/price";
import Loader, { LoaderColor } from "@/components/Loader"; import Loader, { LoaderColor } from "@/components/Loader";
@ -25,9 +23,7 @@ function AddConsultationPage() {
const api = useApi(); const api = useApi();
const tokenFromStore = useSelector(selectors.selectToken); const tokenFromStore = useSelector(selectors.selectToken);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [paymentIntent, setPaymentIntent] = useState< const [paymentIntent, setPaymentIntent] = useState<ResponsePost | null>(null);
ResponsePost | null
>(null);
const [isError, setIsError] = useState(false); const [isError, setIsError] = useState(false);
const returnUrl = `${window.location.protocol}//${ const returnUrl = `${window.location.protocol}//${
window.location.host window.location.host
@ -45,29 +41,34 @@ function AddConsultationPage() {
); );
const handleClick = async () => { const handleClick = async () => {
if (!userFromStore || !currentProduct) return; try {
setIsLoading(true); if (!userFromStore || !currentProduct) return;
const { productId, key } = currentProduct; setIsLoading(true);
const paymentInfo = { const { _id, key } = currentProduct;
productId, const paymentInfo = {
key, productId: _id,
}; key,
const paymentIntent = await createSinglePayment( };
userFromStore, const paymentIntent = await createSinglePayment(
paymentInfo, userFromStore,
tokenFromStore, paymentInfo,
userFromStore.email, tokenFromStore,
userFromStore.profile.full_name, userFromStore.email,
userFromStore.profile.birthday, userFromStore.profile.full_name,
returnUrl, userFromStore.profile.birthday,
api returnUrl,
); api
setPaymentIntent(paymentIntent); );
setIsLoading(false); setPaymentIntent(paymentIntent);
if ("payment" in paymentIntent) { if ("payment" in paymentIntent) {
if (paymentIntent.payment.status === "paid") if (paymentIntent.payment.status === "paid")
return navigate(routes.client.getInformationPartner()); return navigate(routes.client.getInformationPartner());
return setIsError(true); return setIsError(true);
}
} catch (error) {
setIsError(true);
} finally {
setIsLoading(false);
} }
}; };
@ -86,7 +87,7 @@ function AddConsultationPage() {
onClose={() => setPaymentIntent(null)} onClose={() => setPaymentIntent(null)}
> >
<Title variant="h1" className={styles["modal-title"]}> <Title variant="h1" className={styles["modal-title"]}>
{getPriceCentsToDollars(currentProduct?.amount || 0)}$ {getPriceCentsToDollars(currentProduct?.price || 0)}$
</Title> </Title>
<PaymentForm <PaymentForm
stripePublicKey={paymentIntent.paymentIntent.data.public_key} stripePublicKey={paymentIntent.paymentIntent.data.public_key}

View File

@ -9,9 +9,7 @@ import FooterButton from "../../components/FooterButton";
import routes from "@/routes"; import routes from "@/routes";
import PaymentAddress from "../../components/PaymentAddress"; import PaymentAddress from "../../components/PaymentAddress";
import { createSinglePayment } from "@/services/singlePayment"; import { createSinglePayment } from "@/services/singlePayment";
import { import { ResponsePost } from "@/api/resources/SinglePayment";
ResponsePost,
} from "@/api/resources/SinglePayment";
import { useAuth } from "@/auth"; import { useAuth } from "@/auth";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { selectors } from "@/store"; import { selectors } from "@/store";
@ -19,16 +17,14 @@ import { SinglePayment, useApi, useApiCall } from "@/api";
import Loader, { LoaderColor } from "@/components/Loader"; import Loader, { LoaderColor } from "@/components/Loader";
import { getPriceCentsToDollars } from "@/services/price"; import { getPriceCentsToDollars } from "@/services/price";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import PaymentForm from "@/components/pages/PaymentWithEmailPage/PaymentForm"; import PaymentForm from "@/components/pages/SinglePaymentPage/PaymentForm";
function AddReportPage() { function AddReportPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { user: userFromStore } = useAuth(); const { user: userFromStore } = useAuth();
const api = useApi(); const api = useApi();
const tokenFromStore = useSelector(selectors.selectToken); const tokenFromStore = useSelector(selectors.selectToken);
const [paymentIntent, setPaymentIntent] = useState< const [paymentIntent, setPaymentIntent] = useState<ResponsePost | null>(null);
ResponsePost | null
>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false); const [isError, setIsError] = useState(false);
const [activeOffer, setActiveOffer] = useState(signUpOffers[0]); const [activeOffer, setActiveOffer] = useState(signUpOffers[0]);
@ -48,31 +44,36 @@ function AddReportPage() {
}; };
const handleClick = async () => { const handleClick = async () => {
if (!userFromStore || !activeOffer) return; try {
const currentProduct = getCurrentProduct(activeOffer?.productKey); if (!userFromStore || !activeOffer) return;
if (!currentProduct) return; const currentProduct = getCurrentProduct(activeOffer?.productKey);
setIsLoading(true); if (!currentProduct) return;
const { productId, key } = currentProduct; setIsLoading(true);
const paymentInfo = { const { _id, key } = currentProduct;
productId, const paymentInfo = {
key, productId: _id,
}; key,
const paymentIntent = await createSinglePayment( };
userFromStore, const paymentIntent = await createSinglePayment(
paymentInfo, userFromStore,
tokenFromStore, paymentInfo,
userFromStore.email, tokenFromStore,
userFromStore.profile.full_name, userFromStore.email,
userFromStore.profile.birthday, userFromStore.profile.full_name,
returnUrl, userFromStore.profile.birthday,
api returnUrl,
); api
setPaymentIntent(paymentIntent); );
setIsLoading(false); setPaymentIntent(paymentIntent);
if ("payment" in paymentIntent) { if ("payment" in paymentIntent) {
if (paymentIntent.payment.status === "paid") if (paymentIntent.payment.status === "paid")
return navigate(routes.client.unlimitedReadings()); return navigate(routes.client.unlimitedReadings());
return setIsError(true); return setIsError(true);
}
} catch (error) {
setIsError(true);
} finally {
setIsLoading(false);
} }
}; };

View File

@ -16,12 +16,10 @@ import { selectors } from "@/store";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { createSinglePayment } from "@/services/singlePayment"; import { createSinglePayment } from "@/services/singlePayment";
import Loader, { LoaderColor } from "@/components/Loader"; import Loader, { LoaderColor } from "@/components/Loader";
import { import { ResponsePost } from "@/api/resources/SinglePayment";
ResponsePost,
} from "@/api/resources/SinglePayment";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import { getPriceCentsToDollars } from "@/services/price"; import { getPriceCentsToDollars } from "@/services/price";
import PaymentForm from "@/components/pages/PaymentWithEmailPage/PaymentForm"; import PaymentForm from "@/components/pages/SinglePaymentPage/PaymentForm";
const sliderSettings = { const sliderSettings = {
dots: false, dots: false,
@ -40,9 +38,7 @@ function UnlimitedReadingsPage() {
const api = useApi(); const api = useApi();
const tokenFromStore = useSelector(selectors.selectToken); const tokenFromStore = useSelector(selectors.selectToken);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [paymentIntent, setPaymentIntent] = useState< const [paymentIntent, setPaymentIntent] = useState<ResponsePost | null>(null);
ResponsePost | null
>(null);
const [isError, setIsError] = useState(false); const [isError, setIsError] = useState(false);
const returnUrl = `${window.location.protocol}//${ const returnUrl = `${window.location.protocol}//${
window.location.host window.location.host
@ -60,29 +56,34 @@ function UnlimitedReadingsPage() {
); );
const handleClick = async () => { const handleClick = async () => {
if (!userFromStore || !currentProduct) return; try {
setIsLoading(true); if (!userFromStore || !currentProduct) return;
const { productId, key } = currentProduct; setIsLoading(true);
const paymentInfo = { const { _id, key } = currentProduct;
productId, const paymentInfo = {
key, productId: _id,
}; key,
const paymentIntent = await createSinglePayment( };
userFromStore, const paymentIntent = await createSinglePayment(
paymentInfo, userFromStore,
tokenFromStore, paymentInfo,
userFromStore.email, tokenFromStore,
userFromStore.profile.full_name, userFromStore.email,
userFromStore.profile.birthday, userFromStore.profile.full_name,
returnUrl, userFromStore.profile.birthday,
api returnUrl,
); api
setPaymentIntent(paymentIntent); );
setIsLoading(false); setPaymentIntent(paymentIntent);
if ("payment" in paymentIntent) { if ("payment" in paymentIntent) {
if (paymentIntent.payment.status === "paid") if (paymentIntent.payment.status === "paid")
return navigate(routes.client.addConsultation()); return navigate(routes.client.addConsultation());
return setIsError(true); return setIsError(true);
}
} catch (error) {
setIsError(true);
} finally {
setIsLoading(false);
} }
}; };
@ -102,7 +103,7 @@ function UnlimitedReadingsPage() {
onClose={() => setPaymentIntent(null)} onClose={() => setPaymentIntent(null)}
> >
<Title variant="h1" className={styles["modal-title"]}> <Title variant="h1" className={styles["modal-title"]}>
{getPriceCentsToDollars(currentProduct?.amount || 0)}$ {getPriceCentsToDollars(currentProduct?.price || 0)}$
</Title> </Title>
<PaymentForm <PaymentForm
stripePublicKey={paymentIntent.paymentIntent.data.public_key} stripePublicKey={paymentIntent.paymentIntent.data.public_key}

View File

@ -1,7 +1,13 @@
import Title from "@/components/Title"; import Title from "@/components/Title";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
function ComparePrices() { function ComparePrices({
oldPrice,
newPrice,
}: {
oldPrice: string;
newPrice: string;
}) {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={`${styles["old-price"]} ${styles["price-container"]}`}> <div className={`${styles["old-price"]} ${styles["price-container"]}`}>
@ -11,7 +17,7 @@ function ComparePrices() {
</Title> </Title>
</div> </div>
<div className={styles["main-container"]}> <div className={styles["main-container"]}>
<p className={styles.text}>up to $13.67</p> <p className={styles.text}>{oldPrice}</p>
</div> </div>
</div> </div>
<div className={`${styles["new-price"]} ${styles["price-container"]}`}> <div className={`${styles["new-price"]} ${styles["price-container"]}`}>
@ -22,7 +28,7 @@ function ComparePrices() {
</Title> </Title>
</div> </div>
<div className={styles["main-container"]}> <div className={styles["main-container"]}>
<p className={styles.text}>$0</p> <p className={styles.text}>${newPrice}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,13 +1,13 @@
import Title from "@/components/Title"; import Title from "@/components/Title";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
function SpecialOfferBanner() { function SpecialOfferBanner({ title }: { title: string }) {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<img src="/wrapped-gift.webp" alt="Wrapped Gift" /> <img src="/wrapped-gift.webp" alt="Wrapped Gift" />
<div className="text-container"> <div className="text-container">
<Title className={styles.title} variant="h3"> <Title className={styles.title} variant="h3">
Special Offer! {title}
</Title> </Title>
<p className={styles.text}>Everything for free. Trial include!</p> <p className={styles.text}>Everything for free. Trial include!</p>
</div> </div>

View File

@ -15,11 +15,16 @@ import { selectors } from "@/store";
import { getZodiacSignByDate } from "@/services/zodiac-sign"; import { getZodiacSignByDate } from "@/services/zodiac-sign";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import routes from "@/routes"; import routes from "@/routes";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import { EPlacementKeys } from "@/api/resources/Paywall";
function MarketingLanding() { function MarketingLanding() {
const birthdate = useSelector(selectors.selectBirthdate); const birthdate = useSelector(selectors.selectBirthdate);
const zodiacSign = getZodiacSignByDate(birthdate); const zodiacSign = getZodiacSignByDate(birthdate);
const navigate = useNavigate(); const navigate = useNavigate();
const { paywall, products, getText } = usePaywall({
placementKey: EPlacementKeys["aura.placement.email.marketing"],
});
const handleNext = () => { const handleNext = () => {
navigate(routes.client.email("marketing-trial-payment")); navigate(routes.client.email("marketing-trial-payment"));
@ -27,7 +32,7 @@ function MarketingLanding() {
return ( return (
<section className={`${styles.page} page`}> <section className={`${styles.page} page`}>
<SpecialOfferBanner /> <SpecialOfferBanner title={paywall?.name || ""} />
<div className={styles.wrapper}> <div className={styles.wrapper}>
<Title variant="h2" className={`${styles.title} ${styles["hi-title"]}`}> <Title variant="h2" className={`${styles.title} ${styles["hi-title"]}`}>
Hey, {zodiacSign} Sun 👋 Hey, {zodiacSign} Sun 👋
@ -66,7 +71,10 @@ function MarketingLanding() {
alt="Understanding" alt="Understanding"
style={{ minHeight: "323px" }} style={{ minHeight: "323px" }}
/> />
<ComparePrices /> <ComparePrices
oldPrice={getText("text.old.price", {}) as string}
newPrice={`${((products[0]?.trialPrice || 0) / 100).toFixed(2)}`}
/>
<PointsList <PointsList
points={marketingLandingPointsList} points={marketingLandingPointsList}
title="Your plan also includes:" title="Your plan also includes:"

View File

@ -5,29 +5,22 @@ import MainButton from "@/components/MainButton";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import PaymentModal from "../../TrialPayment/components/PaymentModal"; import PaymentModal from "../../TrialPayment/components/PaymentModal";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans"; import { usePaywall } from "@/hooks/paywall/usePaywall";
import { useApi } from "@/api"; import { EPlacementKeys } from "@/api/resources/Paywall";
import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux";
import { actions } from "@/store";
function MarketingTrialPayment() { function MarketingTrialPayment() {
const { i18n } = useTranslation(); const dispatch = useDispatch();
const locale = i18n.language;
const api = useApi();
const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false); 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(() => { useEffect(() => {
(async () => { dispatch(actions.payment.update({ activeProduct: products[0] }));
const { sub_plans } = await api.getSubscriptionPlans({ locale }); }, [dispatch, products]);
const _freeTrialPlan = sub_plans.find(
(subPlan) => subPlan.trial?.is_free
);
setFreeTrialPlan(_freeTrialPlan);
})();
}, [api, locale]);
const openStripeModal = () => { const openStripeModal = () => {
setIsOpenPaymentModal(true); setIsOpenPaymentModal(true);
@ -39,13 +32,17 @@ function MarketingTrialPayment() {
return ( return (
<> <>
<Modal {products[0] && (
containerClassName={styles.modal} <Modal
open={isOpenPaymentModal} containerClassName={styles.modal}
onClose={handleCloseModal} open={isOpenPaymentModal}
> onClose={handleCloseModal}
<PaymentModal activeSubscriptionPlan={freeTrialPlan} /> >
</Modal> <PaymentModal
placementKey={EPlacementKeys["aura.placement.email.marketing"]}
/>
</Modal>
)}
<section className={`${styles.page} page`}> <section className={`${styles.page} page`}>
<div className={styles.wrapper}> <div className={styles.wrapper}>
<div className={styles.banner}>Special Offer</div> <div className={styles.banner}>Special Offer</div>
@ -55,7 +52,7 @@ function MarketingTrialPayment() {
<p className={styles.description}>No pressure. Cancel anytime</p> <p className={styles.description}>No pressure. Cancel anytime</p>
<div className={styles["total-today"]}> <div className={styles["total-today"]}>
<p className={styles.description}>Total today:</p> <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>
<div className={styles.line} /> <div className={styles.line} />
<div className={styles["code-container"]}> <div className={styles["code-container"]}>
@ -74,7 +71,7 @@ function MarketingTrialPayment() {
<p className={styles["sale-description"]}>Save $10 every period</p> <p className={styles["sale-description"]}>Save $10 every period</p>
<div className={styles.line} /> <div className={styles.line} />
<p className={styles["text-description"]}> <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{" "} Subscription <b>renews automatically</b> until cancelled. You{" "}
<b>can cancel at any time</b> before the end of the trial. <b>can cancel at any time</b> before the end of the trial.
</p> </p>

View File

@ -20,6 +20,11 @@ function GenderPage({ productKey }: IGenderPageProps): JSX.Element {
useEffect(() => { useEffect(() => {
const isShowTryApp = targetId === "i"; const isShowTryApp = targetId === "i";
dispatch(actions.userConfig.addIsShowTryApp(isShowTryApp)); dispatch(actions.userConfig.addIsShowTryApp(isShowTryApp));
if (targetId && typeof window.ym === "function" && targetId !== "i") {
window.ym(95799066, "userParams", {
genderFrom: targetId,
});
}
}, [dispatch, targetId]); }, [dispatch, targetId]);
const selectGender = (gender: Gender) => { const selectGender = (gender: Gender) => {

View File

@ -17,7 +17,13 @@ function GetInformationPartnerPage() {
return ( return (
<section className={`${styles.page} page`}> <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/mp4" />
<source src="/videos/background-video-1.mp4" type="video/ogg" /> <source src="/videos/background-video-1.mp4" type="video/ogg" />
Your browser does not support the video tag. Your browser does not support the video tag.

View File

@ -4,6 +4,8 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { onboardingTitles } from "@/data/onboarding"; import { onboardingTitles } from "@/data/onboarding";
import routes from "@/routes"; import routes from "@/routes";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import { EPlacementKeys } from "@/api/resources/Paywall";
function OnboardingPage() { function OnboardingPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -11,6 +13,7 @@ function OnboardingPage() {
const [periodClassName, setPeriodClassName] = useState(""); const [periodClassName, setPeriodClassName] = useState("");
const titleInterval = useRef<NodeJS.Timeout>(); const titleInterval = useRef<NodeJS.Timeout>();
const classNameTimeOut = useRef<NodeJS.Timeout>(); const classNameTimeOut = useRef<NodeJS.Timeout>();
usePaywall({ placementKey: EPlacementKeys["aura.placement.main"] });
const handleNext = useCallback(() => { const handleNext = useCallback(() => {
navigate(routes.client.trialChoice()); navigate(routes.client.trialChoice());

View File

@ -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)}$
</Title>
<PaymentForm
stripePublicKey={paymentIntent.paymentIntent.data.public_key}
clientSecret={paymentIntent.paymentIntent.data.client_secret}
returnUrl={`${returnUrl}?redirect_type=${product?.key}`}
/>
</>
)}
{(!tokenFromStore || !paymentIntent) &&
// || (productId !== "chat.aura" && !name.length)
!isLoadingSinglePayment &&
!isLoadingAuth && (
<>
<NameInput
value={name}
placeholder="Your name"
onValid={handleValidName}
onInvalid={() => setIsValidName(productId !== "chat.aura")}
/>
<EmailInput
name="email"
value={email}
placeholder={t("your_email")}
onValid={handleValidEmail}
onInvalid={() => setIsValidEmail(false)}
/>
<MainButton
className={styles.button}
onClick={handleClick}
disabled={isDisabled}
>
{isLoadingSinglePayment && <Loader color={LoaderColor.White} />}
{!isLoadingSinglePayment &&
!(!apiError && !error && !isLoadingSinglePayment && isAuth) &&
t("_continue")}
{!apiError && !error && !isLoadingSinglePayment && isAuth && (
<img
className={styles["success-icon"]}
src="/SuccessIcon.png"
alt="Success Icon"
/>
)}
</MainButton>
</>
)}
{(error || apiError || errorSinglePayment?.error) && (
<Title variant="h3" style={{ color: "red", margin: 0 }}>
Something went wrong:{" "}
{errorSinglePayment?.error?.length && errorSinglePayment?.error}
</Title>
)}
{apiError && (
<ErrorText
size="medium"
isShown={Boolean(apiError)}
message={apiError ? extractErrorMessage(apiError) : null}
/>
)}
</div>
);
}
export default PaymentWithEmailPage;

View File

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

View File

@ -1,6 +1,6 @@
import { Elements } from "@stripe/react-stripe-js"; import { Elements } from "@stripe/react-stripe-js";
import styles from "./styles.module.css"; 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 { useEffect, useState } from "react";
import { Stripe, loadStripe } from "@stripe/stripe-js"; import { Stripe, loadStripe } from "@stripe/stripe-js";
import SecurityPayments from "../../TrialPayment/components/SecurityPayments"; import SecurityPayments from "../../TrialPayment/components/SecurityPayments";
@ -9,9 +9,15 @@ interface IPaymentFormProps {
stripePublicKey: string; stripePublicKey: string;
clientSecret: string; clientSecret: string;
returnUrl: string; returnUrl: string;
confirmType?: "payment" | "setup";
} }
function PaymentForm({ stripePublicKey, clientSecret, returnUrl }: IPaymentFormProps) { function PaymentForm({
stripePublicKey,
clientSecret,
returnUrl,
confirmType = "payment",
}: IPaymentFormProps) {
const [stripePromise, setStripePromise] = const [stripePromise, setStripePromise] =
useState<Promise<Stripe | null> | null>(null); useState<Promise<Stripe | null> | null>(null);
@ -23,7 +29,7 @@ function PaymentForm({ stripePublicKey, clientSecret, returnUrl }: IPaymentFormP
<div className={styles["payment-method-container"]}> <div className={styles["payment-method-container"]}>
{stripePromise && clientSecret && ( {stripePromise && clientSecret && (
<Elements stripe={stripePromise} options={{ clientSecret }}> <Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm returnUrl={returnUrl} /> <CheckoutForm confirmType={confirmType} returnUrl={returnUrl} />
</Elements> </Elements>
)} )}
</div> </div>

View File

@ -1,6 +1,6 @@
import Title from "@/components/Title"; import Title from "@/components/Title";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import PaymentForm from "../PaymentWithEmailPage/PaymentForm"; import PaymentForm from "./PaymentForm";
import { getPriceCentsToDollars } from "@/services/price"; import { getPriceCentsToDollars } from "@/services/price";
import { useSinglePayment } from "@/hooks/payment/useSinglePayment"; import { useSinglePayment } from "@/hooks/payment/useSinglePayment";
import routes from "@/routes"; import routes from "@/routes";
@ -59,7 +59,7 @@ function SinglePaymentPage({ productId, isForce = false }: ISinglePaymentPage) {
!!tokenFromStore.length && ( !!tokenFromStore.length && (
<> <>
<Title variant="h1" className={styles.title}> <Title variant="h1" className={styles.title}>
{getPriceCentsToDollars(product?.amount || 0)}$ {getPriceCentsToDollars(product?.price || 0)}$
</Title> </Title>
<PaymentForm <PaymentForm
stripePublicKey={paymentIntent.paymentIntent.data.public_key} stripePublicKey={paymentIntent.paymentIntent.data.public_key}

View File

@ -1,73 +1,36 @@
import PriceList from "@/components/PriceList"; import PriceList from "@/components/PriceList";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useState } from "react";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import { useTranslation } from "react-i18next";
import { useApi } from "@/api";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store"; import { actions, selectors } from "@/store";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import routes from "@/routes"; import routes from "@/routes";
import EmailsList from "@/components/EmailsList"; import EmailsList from "@/components/EmailsList";
import MainButton from "@/components/MainButton"; import MainButton from "@/components/MainButton";
import { usePaywall } from "@/hooks/paywall/usePaywall";
interface IPlanKey { import { EPlacementKeys } from "@/api/resources/Paywall";
[key: string]: number; import { getRandomArbitrary } from "@/services/random-value";
}
function TrialChoicePage() { function TrialChoicePage() {
const { i18n } = useTranslation();
const locale = i18n.language;
const api = useApi();
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const selectedPrice = useSelector(selectors.selectSelectedPrice); const selectedPrice = useSelector(selectors.selectSelectedPrice);
const homeConfig = useSelector(selectors.selectHome); const homeConfig = useSelector(selectors.selectHome);
const email = useSelector(selectors.selectEmail); const email = useSelector(selectors.selectEmail);
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
const [isDisabled, setIsDisabled] = useState(true); const [isDisabled, setIsDisabled] = useState(true);
const allowedPlans = useMemo(() => [""], []); const [countUsers, setCountUsers] = useState(752);
useEffect(() => { useEffect(() => {
(async () => { const randomDelay = getRandomArbitrary(3000, 5000);
const { sub_plans } = await api.getSubscriptionPlans({ locale }); const countUsersTimeOut = setTimeout(() => {
const plansWithoutTest = sub_plans.filter( setCountUsers((prevState) => prevState + 1);
(plan: ISubscriptionPlan) => !plan.name.includes("(test)") }, randomDelay);
); return () => clearTimeout(countUsersTimeOut);
const plansKeys: IPlanKey = {}; }, [countUsers]);
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) => { const { products, getText } = usePaywall({
if (!a.trial || !b.trial) { placementKey: EPlacementKeys["aura.placement.main"],
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 handlePriceItem = () => { const handlePriceItem = () => {
setIsDisabled(false); setIsDisabled(false);
@ -85,38 +48,49 @@ function TrialChoicePage() {
return ( return (
<section className={`${styles.page} page`}> <section className={`${styles.page} page`}>
<p className={styles.text}> <p className={styles.text}>
We've helped millions of people to have happier lives and better {getText("text.0", {
relationships, and we want to help you too. replacementSelector: "b",
color: "#1C38EA",
})}
</p> </p>
<p className={`${styles.text} ${styles.bold}`}> <p className={`${styles.text} ${styles.bold}`}>
Money shouldnt stand in the way of finding astrology guidance that {getText("text.1", {
finally works. So, choose an amount that you think is reasonable to try color: "#1C38EA",
us out for one week. })}
</p> </p>
<p className={`${styles.text} ${styles.bold} ${styles.purple}`}> <p className={`${styles.text} ${styles.bold} ${styles.purple}`}>
It costs us $13.67 to offer a 3-day trial, but please choose the amount {getText("text.2", {
you are comfortable with. color: "#1C38EA",
})}
</p> </p>
<div className={styles["price-container"]}> <div className={styles["price-container"]}>
<PriceList <PriceList
subPlans={subPlans} products={products}
activeItem={selectedPrice} activeItem={selectedPrice}
classNameItem={styles["price-item"]} classNameItem={styles["price-item"]}
classNameItemActive={styles["price-item-active"]} classNameItemActive={styles["price-item-active"]}
click={handlePriceItem} click={handlePriceItem}
/> />
<p className={styles["auxiliary-text"]} style={{ maxWidth: "75%" }}> <p className={styles["auxiliary-text"]} style={{ maxWidth: "75%" }}>
This option will help us support those who need to select the lowest {getText("text.3", {
trial prices! color: "#1C38EA",
})}
</p> </p>
<img <img
className={styles["arrow-image"]} className={styles["arrow-image"]}
src="/arrow.svg" src="/arrow.svg"
alt={`Arrow to $${subPlans.at(-1)}`} alt={`Arrow to $${products.at(-1)?.name}`}
/> />
</div> </div>
<div className={styles["emails-list-container"]}> <div className={styles["emails-list-container"]}>
<EmailsList <EmailsList
title={getText("text.5", {
replacementSelector: "strong",
replacement: {
target: "${quantity}",
replacement: countUsers.toString(),
},
})}
classNameContainer={styles["emails-container"]} classNameContainer={styles["emails-container"]}
classNameTitle={styles["emails-title"]} classNameTitle={styles["emails-title"]}
classNameEmailItem={styles["email-item"]} classNameEmailItem={styles["email-item"]}
@ -129,10 +103,14 @@ function TrialChoicePage() {
disabled={isDisabled} disabled={isDisabled}
onClick={handleNext} onClick={handleNext}
> >
See my plan {getText("text.button.1", {
color: "#1C38EA",
})}
</MainButton> </MainButton>
<p className={styles["auxiliary-text"]}> <p className={styles["auxiliary-text"]}>
*Cost of trial as of February 2024 {getText("text.4", {
color: "#1C38EA",
})}
</p> </p>
</section> </section>
); );

View File

@ -4,48 +4,62 @@ import PaymentMethodsChoice from "../PaymentMethodsChoice";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods"; import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods";
import { Elements } from "@stripe/react-stripe-js"; import { Elements } from "@stripe/react-stripe-js";
import ApplePayButton from "@/components/StripePage/ApplePayButton"; import ApplePayButton from "@/components/PaymentPage/methods/ApplePayButton";
import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm"; import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm";
import { Stripe, loadStripe } from "@stripe/stripe-js"; import { Stripe, loadStripe } from "@stripe/stripe-js";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { selectors } from "@/store"; 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 Loader from "@/components/Loader";
import { getPriceFromTrial } from "@/services/price";
import SecurityPayments from "../SecurityPayments"; 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 { interface IPaymentModalProps {
activeSubscriptionPlan?: ISubscriptionPlan; activeProduct?: IPaywallProduct;
noTrial?: boolean; noTrial?: boolean;
returnUrl?: string; returnUrl?: string;
placementKey?: EPlacementKeys;
} }
const getPrice = (product: IPaywallProduct | null) => {
if (!product) {
return 0;
}
return (product.trialPrice === 100 ? 99 : product.trialPrice || 0) / 100;
};
function PaymentModal({ function PaymentModal({
activeSubscriptionPlan, activeProduct,
noTrial, noTrial,
returnUrl, returnUrl,
placementKey = EPlacementKeys["aura.placement.main"],
}: IPaymentModalProps) { }: 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] = const [stripePromise, setStripePromise] =
useState<Promise<Stripe | null> | null>(null); useState<Promise<Stripe | null> | null>(null);
const [clientSecret, setClientSecret] = useState<string>("");
const [subscriptionReceiptId, setSubscriptionReceiptId] = const { products } = usePaywall({
useState<string>(""); placementKey,
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(() => { const paymentMethodsButtons = useMemo(() => {
// return paymentMethods.filter( // return paymentMethods.filter(
@ -64,43 +78,10 @@ function PaymentModal({
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const siteConfig = await api.getAppConfig({ bundleId: "auraweb" }); if (!products?.length || !publicKey) return;
setStripePromise(loadStripe(siteConfig.data.stripe_public_key)); setStripePromise(loadStripe(publicKey));
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]); }, [products, publicKey]);
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]);
if (isLoading) { if (isLoading) {
return ( return (
@ -112,7 +93,7 @@ function PaymentModal({
); );
} }
if (isError) { if (error?.length) {
return ( return (
<div className={styles["payment-modal"]}> <div className={styles["payment-modal"]}>
<Title variant="h3" className={styles.title}> <Title variant="h3" className={styles.title}>
@ -132,16 +113,13 @@ function PaymentModal({
selectedPaymentMethod={selectedPaymentMethod} selectedPaymentMethod={selectedPaymentMethod}
onSelectPaymentMethod={onSelectPaymentMethod} onSelectPaymentMethod={onSelectPaymentMethod}
/> />
{activeSubPlan && ( {_activeProduct && (
<div> <div>
{!noTrial && ( {!noTrial && (
<> <>
<p className={styles["sub-plan-description"]}> <p className={styles["sub-plan-description"]}>
You will be charged only{" "} You will be charged only{" "}
<b> <b>${getPrice(_activeProduct)} for your 3-day trial.</b>
${getPriceFromTrial(activeSubPlan?.trial)} for your 3-day
trial.
</b>
</p> </p>
<p className={styles["sub-plan-description"]}> <p className={styles["sub-plan-description"]}>
We`ll <b>email you a reminder</b> before your trial period ends. We`ll <b>email you a reminder</b> before your trial period ends.
@ -160,16 +138,17 @@ function PaymentModal({
{selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && ( {selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && (
<div className={styles["payment-method"]}> <div className={styles["payment-method"]}>
<ApplePayButton <ApplePayButton
activeSubPlan={activeSubPlan} activeProduct={_activeProduct}
client_secret={clientSecret} client_secret={clientSecret}
subscriptionReceiptId={subscriptionReceiptId} subscriptionReceiptId={paymentIntentId}
/> />
</div> </div>
)} )}
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && ( {selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
<CheckoutForm <CheckoutForm
subscriptionReceiptId={subscriptionReceiptId} confirmType={paymentType}
subscriptionReceiptId={paymentIntentId}
returnUrl={returnUrl} returnUrl={returnUrl}
/> />
)} )}

View File

@ -1,18 +1,21 @@
import Title from "@/components/Title"; import Title from "@/components/Title";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { getPriceFromTrial } from "@/services/price";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import CustomButton from "../CustomButton"; import CustomButton from "../CustomButton";
import GuardPayments from "../GuardPayments"; import GuardPayments from "../GuardPayments";
import { useState } from "react"; import { useState } from "react";
import FullScreenModal from "@/components/FullScreenModal"; import FullScreenModal from "@/components/FullScreenModal";
import { IPaywallProduct } from "@/api/resources/Paywall";
interface IPaymentTableProps { interface IPaymentTableProps {
subPlan: ISubscriptionPlan; product: IPaywallProduct;
buttonClick: () => void; 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 [isOpenPrivacyModal, setIsOpenPrivacyModal] = useState<boolean>(false);
const handleSubscriptionPolicyClick = (event: React.MouseEvent) => { const handleSubscriptionPolicyClick = (event: React.MouseEvent) => {
event.preventDefault(); event.preventDefault();
@ -44,20 +47,18 @@ function PaymentTable({ subPlan, buttonClick }: IPaymentTableProps) {
<div className={styles["table-container"]}> <div className={styles["table-container"]}>
<Title variant="h3" className={styles.title}> <Title variant="h3" className={styles.title}>
Personalized reading for{" "} Personalized reading for{" "}
<span className={styles.purple}> <span className={styles.purple}>${getPrice(product)}</span>
${getPriceFromTrial(subPlan?.trial)}
</span>
</Title> </Title>
<div className={styles["table-element"]}> <div className={styles["table-element"]}>
<p>Total today:</p> <p>Total today:</p>
<span>${getPriceFromTrial(subPlan?.trial)}</span> <span>${getPrice(product)}</span>
</div> </div>
<hr /> <hr />
<div className={styles["table-element"]}> <div className={styles["table-element"]}>
<p>Your cost per 2 weeks after trial</p> <p>Your cost per 2 weeks after trial</p>
<div> <div>
<span className={styles.discount}>$65</span> <span className={styles.discount}>$65</span>
<span>${subPlan.price_cents / 100}</span> <span>${product.trialPrice / 100}</span>
</div> </div>
</div> </div>
</div> </div>
@ -69,9 +70,9 @@ function PaymentTable({ subPlan, buttonClick }: IPaymentTableProps) {
<p className={styles.policy}> <p className={styles.policy}>
You are enrolling in 2 weeks subscription. By continuing you agree that 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 $ if you don't cancel prior to the end of the 3-day trial for the $
{getPriceFromTrial(subPlan?.trial)} you will automatically be charged {getPrice(product)} you will automatically be charged $19 every 2 weeks
$19 every 2 weeks until you cancel in settings. Learn more about until you cancel in settings. Learn more about cancellation and refund
cancellation and refund policy in{" "} policy in{" "}
<a onClick={handleSubscriptionPolicyClick}>Subscription policy</a> <a onClick={handleSubscriptionPolicyClick}>Subscription policy</a>
</p> </p>
</> </>

View File

@ -13,22 +13,17 @@ import YourReading from "./components/YourReading";
import Reviews from "./components/Reviews"; import Reviews from "./components/Reviews";
import PointsList from "./components/PointsList"; import PointsList from "./components/PointsList";
import OftenAsk from "./components/OftenAsk"; import OftenAsk from "./components/OftenAsk";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useApi } from "@/api";
import { getClientLocale } from "@/locales";
import { Locale } from "@/components/PaymentTable";
import WithPartnerInformation from "./components/WithPartnerInformation"; import WithPartnerInformation from "./components/WithPartnerInformation";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import PaymentModal from "./components/PaymentModal"; import PaymentModal from "./components/PaymentModal";
import { trialPaymentPointsList } from "@/data/pointsLists"; import { trialPaymentPointsList } from "@/data/pointsLists";
import { trialPaymentReviews } from "@/data/reviews"; import { trialPaymentReviews } from "@/data/reviews";
import { usePaywall } from "@/hooks/paywall/usePaywall";
const locale = getClientLocale() as Locale; import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
function TrialPaymentPage() { function TrialPaymentPage() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const api = useApi();
const navigate = useNavigate(); const navigate = useNavigate();
const birthdate = useSelector(selectors.selectBirthdate); const birthdate = useSelector(selectors.selectBirthdate);
const zodiacSign = getZodiacSignByDate(birthdate); const zodiacSign = getZodiacSignByDate(birthdate);
@ -42,55 +37,46 @@ function TrialPaymentPage() {
flowChoice, flowChoice,
} = useSelector(selectors.selectQuestionnaire); } = useSelector(selectors.selectQuestionnaire);
const partnerZodiacSign = getZodiacSignByDate(partnerBirthdate); const partnerZodiacSign = getZodiacSignByDate(partnerBirthdate);
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan);
const [activeSubPlan, setActiveSubPlan] = useState<ISubscriptionPlan | null>(
activeSubPlanFromStore
);
const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false); const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false);
const [marginTopTitle, setMarginTopTitle] = useState<number>(360); const [marginTopTitle, setMarginTopTitle] = useState<number>(360);
const [singleOrWithPartner, setSingleOrWithPartner] = useState< const [singleOrWithPartner, setSingleOrWithPartner] = useState<
"single" | "partner" "single" | "partner"
>("single"); >("single");
const { subPlan } = useParams(); const { subPlan } = useParams();
const { products } = usePaywall({
useEffect(() => { placementKey: EPlacementKeys["aura.placement.main"],
(async () => { });
const { sub_plans } = await api.getSubscriptionPlans({ locale }); const activeProductFromStore = useSelector(selectors.selectActiveProduct);
const plans = sub_plans const [activeProduct, setActiveProduct] = useState<IPaywallProduct | null>(
.filter((plan: ISubscriptionPlan) => plan.provider === "stripe") activeProductFromStore
.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(() => { useEffect(() => {
if (subPlan) { if (subPlan) {
const targetSubPlan = subPlans.find( const targetProduct = products.find(
(sub_plan) => (product) =>
String( String(
sub_plan?.trial?.price_cents product?.trialPrice
? Math.floor((sub_plan?.trial?.price_cents + 1) / 100) ? Math.floor((product?.trialPrice + 1) / 100)
: sub_plan.id.replace(".", "") : product.key.replace(".", "")
) === subPlan ) === subPlan
); );
if (targetSubPlan) { if (targetProduct) {
setActiveSubPlan(targetSubPlan); setActiveProduct(targetProduct);
dispatch(actions.payment.update({ activeSubPlan: targetSubPlan })); 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(() => { useEffect(() => {
if (["relationship", "married"].includes(flowChoice)) { if (["relationship", "married"].includes(flowChoice)) {
@ -102,7 +88,7 @@ function TrialPaymentPage() {
setMarginTopTitle(340); setMarginTopTitle(340);
}, [flowChoice]); }, [flowChoice]);
if (!activeSubPlan) { if (!activeProduct) {
return <Navigate to={routes.client.trialChoice()} />; return <Navigate to={routes.client.trialChoice()} />;
} }
@ -157,7 +143,7 @@ function TrialPaymentPage() {
Your Personalized Clarity & Love Reading is ready! Your Personalized Clarity & Love Reading is ready!
</Title> </Title>
<Goal goal={goal} /> <Goal goal={goal} />
<PaymentTable subPlan={activeSubPlan} buttonClick={openStripeModal} /> <PaymentTable product={activeProduct} buttonClick={openStripeModal} />
<YourReading <YourReading
gender={gender} gender={gender}
zodiacSign={zodiacSign} zodiacSign={zodiacSign}
@ -174,7 +160,7 @@ function TrialPaymentPage() {
<Reviews reviews={trialPaymentReviews} /> <Reviews reviews={trialPaymentReviews} />
<PointsList title="What you get" points={trialPaymentPointsList} /> <PointsList title="What you get" points={trialPaymentPointsList} />
<OftenAsk /> <OftenAsk />
<PaymentTable subPlan={activeSubPlan} buttonClick={openStripeModal} /> <PaymentTable product={activeProduct} buttonClick={openStripeModal} />
</section> </section>
); );
} }

Some files were not shown because too many files have changed in this diff Show More