Merge branch 'develop' into 'main'
Develop See merge request witapp/aura-webapp!154
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 44 KiB |
BIN
public/questionnaire-redesign/zodiacs/male/pdf.sex.LEO.MALE.webp
Normal file
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 22 KiB |
@ -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
|
||||||
|
|||||||
46
src/api/resources/Payment.ts
Normal 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 });
|
||||||
|
};
|
||||||
67
src/api/resources/Paywall.ts
Normal 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) });
|
||||||
|
};
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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) })
|
||||||
|
}
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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 />} />
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
@ -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
|
||||||
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from './Button'
|
|
||||||
export * from './Modal'
|
|
||||||
@ -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") {
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -15,4 +15,5 @@
|
|||||||
.image {
|
.image {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 8px;
|
bottom: 8px;
|
||||||
|
z-index: -1;
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
152
src/components/pages/ABDesign/v1/data/zodiacSignsInfo.ts
Normal 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.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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]}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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 shouldn’t 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -137,4 +137,11 @@
|
|||||||
|
|
||||||
.email {
|
.email {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
@ -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}`}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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:"
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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());
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -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%;
|
|
||||||
}
|
|
||||||
@ -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>
|
||||||
@ -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}
|
||||||
|
|||||||
@ -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 shouldn’t 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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||