Merge branch 'AW-79-AW-80-AW-81-newPaymentMethod-Placement' into 'develop'
AW-79-AW-80-AW-81-newPaymentMethod-Placement See merge request witapp/aura-webapp!139
This commit is contained in:
commit
b3707593c6
@ -1,5 +1,6 @@
|
||||
AURA_API_HOST=https://api-web.aura.wit.life
|
||||
AURA_DAPI_HOST=https://dev.api.aura.witapps.us
|
||||
AURA_DAPI_PREFIX=v2
|
||||
AURA_SITE_HOST=https://aura.wit.life
|
||||
AURA_PREFIX=api/v1
|
||||
AURA_OPEN_AI_HOST=https://api.openai.com
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
AURA_API_HOST=https://api-web.aura.wit.life
|
||||
AURA_DAPI_HOST=https://api.aura.witapps.us
|
||||
AURA_DAPI_PREFIX=v2
|
||||
AURA_SITE_HOST=https://aura.wit.life
|
||||
AURA_PREFIX=api/v1
|
||||
AURA_OPEN_AI_HOST=https://api.openai.com
|
||||
|
||||
@ -11,7 +11,6 @@ import {
|
||||
DailyForecasts,
|
||||
SubscriptionItems,
|
||||
SubscriptionCheckout,
|
||||
SubscriptionReceipts,
|
||||
SubscriptionStatus,
|
||||
AICompatCategories,
|
||||
AICompats,
|
||||
@ -28,6 +27,8 @@ import {
|
||||
SinglePayment,
|
||||
Products,
|
||||
Palmistry,
|
||||
Paywall,
|
||||
Payment,
|
||||
} from './resources'
|
||||
|
||||
const api = {
|
||||
@ -48,8 +49,6 @@ const api = {
|
||||
getSubscriptionPlans: createMethod<SubscriptionPlans.Payload, SubscriptionPlans.Response>(SubscriptionPlans.createRequest),
|
||||
getSubscriptionCheckout: createMethod<SubscriptionCheckout.Payload, SubscriptionCheckout.Response>(SubscriptionCheckout.createRequest),
|
||||
getSubscriptionStatus: createMethod<SubscriptionStatus.Payload, SubscriptionStatus.Response>(SubscriptionStatus.createRequest),
|
||||
getSubscriptionReceipt: createMethod<SubscriptionReceipts.GetPayload, SubscriptionReceipts.Response>(SubscriptionReceipts.createGetRequest),
|
||||
createSubscriptionReceipt: createMethod<SubscriptionReceipts.Payload, SubscriptionReceipts.Response>(SubscriptionReceipts.createRequest),
|
||||
getAiCompatCategories: createMethod<AICompatCategories.Payload, AICompatCategories.Response>(AICompatCategories.createRequest),
|
||||
getAiCompat: createMethod<AICompats.Payload, AICompats.Response>(AICompats.createRequest),
|
||||
getAiRequest: createMethod<AIRequests.Payload, AIRequests.Response>(AIRequests.createRequest),
|
||||
@ -75,6 +74,10 @@ const api = {
|
||||
getPalmistryLines: createMethod<Palmistry.Payload, Palmistry.Response>(Palmistry.createRequest),
|
||||
// New Authorization
|
||||
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
|
||||
|
||||
46
src/api/resources/Payment.ts
Normal file
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 });
|
||||
};
|
||||
64
src/api/resources/Paywall.ts
Normal file
64
src/api/resources/Paywall.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import routes from "@/routes";
|
||||
import { getAuthHeaders } from "../utils";
|
||||
|
||||
interface Payload {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface PayloadGet extends Payload {
|
||||
placementKey: EPlacementKeys;
|
||||
}
|
||||
|
||||
export enum EPlacementKeys {
|
||||
"aura.placement.main" = "aura.placement.main",
|
||||
"aura.placement.redesign.main" = "aura.placement.redesign.main",
|
||||
}
|
||||
|
||||
interface ResponseGetSuccess {
|
||||
paywall: IPaywall;
|
||||
}
|
||||
|
||||
interface ResponseGetError {
|
||||
status: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface IPaywall {
|
||||
_id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
products: IPaywallProduct[];
|
||||
properties: IPaywallProperties[];
|
||||
}
|
||||
|
||||
export interface IPaywallProduct {
|
||||
_id: string;
|
||||
key: string;
|
||||
productId: string;
|
||||
name: string;
|
||||
priceId: string;
|
||||
type: string;
|
||||
description: string;
|
||||
discountPrice: null;
|
||||
discountPriceId: null;
|
||||
isDiscount: boolean;
|
||||
isFreeTrial: boolean;
|
||||
isTrial: boolean;
|
||||
price: number;
|
||||
trialDuration: number;
|
||||
trialPrice: number;
|
||||
trialPriceId: string;
|
||||
}
|
||||
|
||||
interface IPaywallProperties {
|
||||
_id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type ResponseGet = ResponseGetSuccess | ResponseGetError;
|
||||
|
||||
export const createRequestGet = ({ token, placementKey }: PayloadGet): Request => {
|
||||
const url = new URL(routes.server.getPaywallByPlacementKey(placementKey));
|
||||
return new Request(url, { method: "GET", headers: getAuthHeaders(token) });
|
||||
};
|
||||
@ -10,7 +10,6 @@ export * as AuthTokens from "./AuthTokens";
|
||||
export * as SubscriptionItems from "./UserSubscriptionItemPrices";
|
||||
export * as SubscriptionCheckout from "./UserSubscriptionCheckout";
|
||||
export * as SubscriptionStatus from "./UserSubscriptionStatus";
|
||||
export * as SubscriptionReceipts from "./UserSubscriptionReceipts";
|
||||
export * as AICompatCategories from "./AICompatCategories";
|
||||
export * as AICompats from "./AICompats";
|
||||
export * as AIRequests from "./AIRequests";
|
||||
@ -26,3 +25,5 @@ export * as OpenAI from "./OpenAI";
|
||||
export * as SinglePayment from "./SinglePayment";
|
||||
export * as Products from "./Products";
|
||||
export * as Palmistry from "./Palmistry";
|
||||
export * as Paywall from "./Paywall";
|
||||
export * as Payment from "./Payment";
|
||||
|
||||
@ -4,7 +4,6 @@ import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { actions, selectors } from "@/store";
|
||||
import { useApi } from "@/api";
|
||||
import Title from "../Title";
|
||||
import Policy from "../Policy";
|
||||
import EmailInput from "./EmailInput";
|
||||
@ -12,9 +11,10 @@ import MainButton from "../MainButton";
|
||||
import Loader, { LoaderColor } from "../Loader";
|
||||
import routes from "@/routes";
|
||||
import NameInput from "./NameInput";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import { useAuthentication } from "@/hooks/authentication/use-authentication";
|
||||
import { ESourceAuthorization } from "@/api/resources/User";
|
||||
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
|
||||
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
||||
|
||||
interface IEmailEnterPage {
|
||||
redirectUrl?: string;
|
||||
@ -25,8 +25,7 @@ function EmailEnterPage({
|
||||
redirectUrl = routes.client.emailConfirm(),
|
||||
isRequiredName = false,
|
||||
}: IEmailEnterPage): JSX.Element {
|
||||
const api = useApi();
|
||||
const { t, i18n } = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const [email, setEmail] = useState("");
|
||||
@ -35,51 +34,31 @@ function EmailEnterPage({
|
||||
const [isValidEmail, setIsValidEmail] = useState(false);
|
||||
const [isValidName, setIsValidName] = useState(!isRequiredName);
|
||||
const [isAuth, setIsAuth] = useState(false);
|
||||
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
|
||||
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan);
|
||||
const [activeSubPlan, setActiveSubPlan] = useState<ISubscriptionPlan | null>(
|
||||
activeSubPlanFromStore
|
||||
);
|
||||
const locale = i18n.language;
|
||||
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
|
||||
const { subPlan } = useParams();
|
||||
const { error, isLoading, authorization } = useAuthentication();
|
||||
const { products } = usePaywall({
|
||||
placementKey: EPlacementKeys["aura.placement.main"],
|
||||
});
|
||||
const [activeProduct, setActiveProduct] = useState<IPaywallProduct | null>(
|
||||
activeProductFromStore
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (subPlan) {
|
||||
const targetSubPlan = subPlans.find(
|
||||
(sub_plan) =>
|
||||
const targetProduct = products.find(
|
||||
(product) =>
|
||||
String(
|
||||
sub_plan?.trial?.price_cents
|
||||
? Math.floor((sub_plan?.trial?.price_cents + 1) / 100)
|
||||
: sub_plan.id.replace(".", "")
|
||||
product?.trialPrice
|
||||
? Math.floor((product?.trialPrice + 1) / 100)
|
||||
: product.key.replace(".", "")
|
||||
) === subPlan
|
||||
);
|
||||
if (targetSubPlan) {
|
||||
setActiveSubPlan(targetSubPlan);
|
||||
if (targetProduct) {
|
||||
setActiveProduct(targetProduct);
|
||||
}
|
||||
}
|
||||
}, [subPlan, subPlans]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { sub_plans } = await api.getSubscriptionPlans({ locale });
|
||||
const plans = sub_plans
|
||||
.filter((plan: ISubscriptionPlan) => plan.provider === "stripe")
|
||||
.sort((a, b) => {
|
||||
if (!a.trial || !b.trial) {
|
||||
return 0;
|
||||
}
|
||||
if (a?.trial?.price_cents < b?.trial?.price_cents) {
|
||||
return -1;
|
||||
}
|
||||
if (a?.trial?.price_cents > b?.trial?.price_cents) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
setSubPlans(plans);
|
||||
})();
|
||||
}, [api, locale]);
|
||||
}, [subPlan, products]);
|
||||
|
||||
const handleValidEmail = (email: string) => {
|
||||
dispatch(actions.form.addEmail(email));
|
||||
@ -122,7 +101,7 @@ function EmailEnterPage({
|
||||
await authorization(email, source);
|
||||
dispatch(
|
||||
actions.payment.update({
|
||||
activeSubPlan,
|
||||
activeProduct,
|
||||
})
|
||||
);
|
||||
setIsAuth(true);
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { getRandomArbitrary, getRandomName } from "@/services/random-value";
|
||||
import EmailItem, { IEmailItemProps } from "../EmailItem";
|
||||
import styles from "./styles.module.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const getEmails = (): IEmailItemProps[] => {
|
||||
@ -18,6 +17,7 @@ const getEmails = (): IEmailItemProps[] => {
|
||||
};
|
||||
|
||||
interface IEmailsListProps {
|
||||
title: string | JSX.Element | JSX.Element[];
|
||||
classNameContainer?: string;
|
||||
classNameTitle?: string;
|
||||
classNameEmailItem?: string;
|
||||
@ -25,25 +25,16 @@ interface IEmailsListProps {
|
||||
}
|
||||
|
||||
function EmailsList({
|
||||
title,
|
||||
classNameContainer = "",
|
||||
classNameTitle = "",
|
||||
classNameEmailItem = "",
|
||||
direction = "up-down",
|
||||
}: IEmailsListProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const [countUsers, setCountUsers] = useState(752);
|
||||
const [emails, setEmails] = useState(getEmails());
|
||||
const [elementIdx, setElementIdx] = useState(0);
|
||||
const itemsRef = useRef<HTMLDivElement[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const randomDelay = getRandomArbitrary(3000, 5000);
|
||||
const countUsersTimeOut = setTimeout(() => {
|
||||
setCountUsers((prevState) => prevState + 1);
|
||||
}, randomDelay);
|
||||
return () => clearTimeout(countUsersTimeOut);
|
||||
}, [countUsers]);
|
||||
|
||||
useEffect(() => {
|
||||
let randomDelay = getRandomArbitrary(500, 5000);
|
||||
if (!elementIdx) {
|
||||
@ -69,11 +60,7 @@ function EmailsList({
|
||||
|
||||
return (
|
||||
<div className={`${styles.container} ${classNameContainer}`}>
|
||||
<span className={`${styles["title"]} ${classNameTitle}`}>
|
||||
{t("people_joined_today", {
|
||||
countPeoples: <strong>{countUsers}</strong>,
|
||||
})}
|
||||
</span>
|
||||
<span className={`${styles["title"]} ${classNameTitle}`}>{title}</span>
|
||||
<div className={`${styles["emails-container"]} ${styles[direction]}`}>
|
||||
{emails.map(({ email, price }, idx) => (
|
||||
<div
|
||||
|
||||
@ -16,12 +16,14 @@ interface ICheckoutFormProps {
|
||||
children?: JSX.Element | null;
|
||||
subscriptionReceiptId?: string;
|
||||
returnUrl?: string;
|
||||
confirmType?: "payment" | "setup";
|
||||
}
|
||||
|
||||
export default function CheckoutForm({
|
||||
children,
|
||||
subscriptionReceiptId,
|
||||
returnUrl,
|
||||
confirmType = "payment",
|
||||
}: ICheckoutFormProps) {
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
@ -42,7 +44,9 @@ export default function CheckoutForm({
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
const { error } = await stripe.confirmPayment({
|
||||
const { error } = await stripe[
|
||||
confirmType === "payment" ? "confirmPayment" : "confirmSetup"
|
||||
]({
|
||||
elements,
|
||||
confirmParams: {
|
||||
return_url: returnUrl
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
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";
|
||||
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
||||
import { EPlacementKeys } from "@/api/resources/Paywall";
|
||||
import { useMakePayment } from "@/hooks/payment/useMakePayment";
|
||||
|
||||
interface StripeModalProps {
|
||||
open: boolean;
|
||||
@ -29,59 +29,63 @@ export function StripeModal({
|
||||
}: // 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) {
|
||||
|
||||
const { products } = usePaywall({
|
||||
placementKey: EPlacementKeys["aura.placement.main"],
|
||||
});
|
||||
|
||||
const activeProduct = useSelector(selectors.selectActiveProduct);
|
||||
if (!activeProduct) {
|
||||
navigate(routes.client.trialChoice());
|
||||
}
|
||||
|
||||
const {
|
||||
paymentIntentId,
|
||||
clientSecret,
|
||||
returnUrl: checkoutUrl,
|
||||
paymentType,
|
||||
publicKey,
|
||||
isLoading,
|
||||
error,
|
||||
} = useMakePayment({
|
||||
productId: activeProduct?._id || "",
|
||||
});
|
||||
|
||||
if (checkoutUrl?.length) {
|
||||
window.location.href = checkoutUrl;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const siteConfig = await api.getAppConfig({ bundleId: "auraweb" });
|
||||
setStripePromise(loadStripe(siteConfig.data.stripe_public_key));
|
||||
const { sub_plans } = await api.getSubscriptionPlans({ locale });
|
||||
const isActiveSubPlan = sub_plans.find(
|
||||
(subPlan) => subPlan.id === activeSubPlan?.id
|
||||
if (!products?.length || !publicKey) return;
|
||||
setStripePromise(loadStripe(publicKey));
|
||||
const isActiveSubPlan = products.find(
|
||||
(product) => product._id === activeProduct?._id
|
||||
);
|
||||
if (!activeSubPlan || !isActiveSubPlan) {
|
||||
if (!activeProduct || !isActiveSubPlan) {
|
||||
navigate(routes.client.priceList());
|
||||
}
|
||||
})();
|
||||
}, [activeSubPlan, api, locale, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { subscription_receipt } = await api.createSubscriptionReceipt({
|
||||
token,
|
||||
way: "stripe",
|
||||
subscription_receipt: {
|
||||
sub_plan_id: activeSubPlan?.id || "stripe.7",
|
||||
},
|
||||
});
|
||||
const { id } = subscription_receipt;
|
||||
const { client_secret } = subscription_receipt.data;
|
||||
setSubscriptionReceiptId(id);
|
||||
setClientSecret(client_secret);
|
||||
setIsLoading(false);
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [api, token]);
|
||||
}, [activeProduct, navigate, products, publicKey]);
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (error?.length) {
|
||||
return (
|
||||
<div className={styles["payment-modal"]}>
|
||||
<Title variant="h3" className={styles.title}>
|
||||
Something went wrong
|
||||
</Title>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={handleClose}>
|
||||
{isLoading ? (
|
||||
@ -97,17 +101,18 @@ StripeModalProps): JSX.Element {
|
||||
<p className={styles.email}>{email}</p>
|
||||
</>
|
||||
)}
|
||||
{stripePromise && clientSecret && subscriptionReceiptId && (
|
||||
{stripePromise && clientSecret && paymentIntentId && (
|
||||
<Elements stripe={stripePromise} options={{ clientSecret }}>
|
||||
<ApplePayButton
|
||||
activeSubPlan={activeSubPlan}
|
||||
activeProduct={activeProduct}
|
||||
client_secret={clientSecret}
|
||||
subscriptionReceiptId={subscriptionReceiptId}
|
||||
subscriptionReceiptId={paymentIntentId}
|
||||
/>
|
||||
{activeProduct && <SubPlanInformation product={activeProduct} />}
|
||||
<CheckoutForm
|
||||
confirmType={paymentType}
|
||||
subscriptionReceiptId={paymentIntentId}
|
||||
/>
|
||||
{activeSubPlan && (
|
||||
<SubPlanInformation subPlan={activeSubPlan} />
|
||||
)}
|
||||
<CheckoutForm subscriptionReceiptId={subscriptionReceiptId} />
|
||||
</Elements>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@ -3,89 +3,17 @@ import { useEffect, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { actions } from "@/store";
|
||||
// import { SubscriptionReceipts, useApi, useApiCall } from "@/api";
|
||||
// import { useAuth } from "@/auth";
|
||||
import styles from "./styles.module.css";
|
||||
import Loader from "@/components/Loader";
|
||||
import { paymentResultPathsOfProducts } from "@/data/products";
|
||||
|
||||
function PaymentResultPage(): JSX.Element {
|
||||
// const api = useApi();
|
||||
// const { token } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const [searchParams] = useSearchParams();
|
||||
const status = searchParams.get("redirect_status");
|
||||
const redirect_type = searchParams.get("redirect_type");
|
||||
// const { id } = useParams();
|
||||
// const requestTimeOutRef = useRef<NodeJS.Timeout>();
|
||||
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(() => {
|
||||
if (status === "succeeded") {
|
||||
|
||||
@ -3,37 +3,39 @@ import PriceItem from "../PriceItem";
|
||||
import styles from "./styles.module.css";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { actions } from "@/store";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import { IPaywallProduct } from "@/api/resources/Paywall";
|
||||
|
||||
interface PriceListProps {
|
||||
subPlans: ISubscriptionPlan[];
|
||||
products: IPaywallProduct[];
|
||||
activeItem: number | null;
|
||||
classNameItem?: string;
|
||||
classNameItemActive?: string;
|
||||
click: () => void;
|
||||
}
|
||||
|
||||
const getPrice = (plan: ISubscriptionPlan) => {
|
||||
return (plan.trial?.price_cents || 0) / 100;
|
||||
const getPrice = (product: IPaywallProduct) => {
|
||||
return (product.trialPrice || 0) / 100;
|
||||
};
|
||||
|
||||
function PriceList({
|
||||
click,
|
||||
subPlans,
|
||||
products,
|
||||
classNameItem = "",
|
||||
classNameItemActive = "",
|
||||
}: PriceListProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const [activePlanItem, setActivePlanItem] =
|
||||
useState<ISubscriptionPlan | null>(null);
|
||||
const [activeProduct, setActiveProduct] = useState<IPaywallProduct | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const priceItemClick = (id: string) => {
|
||||
const activePlan = subPlans.find((item) => item.id === String(id)) || null;
|
||||
setActivePlanItem(activePlan);
|
||||
if (activePlan) {
|
||||
const activeProduct =
|
||||
products.find((item) => item._id === String(id)) || null;
|
||||
setActiveProduct(activeProduct);
|
||||
if (activeProduct) {
|
||||
dispatch(
|
||||
actions.payment.update({
|
||||
activeSubPlan: activePlan,
|
||||
activeProduct,
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -42,12 +44,12 @@ function PriceList({
|
||||
|
||||
return (
|
||||
<div className={`${styles.container}`}>
|
||||
{subPlans.map((plan, idx) => (
|
||||
{products.map((product, idx) => (
|
||||
<PriceItem
|
||||
active={plan.id === activePlanItem?.id}
|
||||
active={product._id === activeProduct?._id}
|
||||
key={idx}
|
||||
value={getPrice(plan)}
|
||||
id={plan.id}
|
||||
value={getPrice(product)}
|
||||
id={product._id}
|
||||
className={classNameItem}
|
||||
classNameActive={classNameItemActive}
|
||||
click={priceItemClick}
|
||||
|
||||
@ -8,42 +8,31 @@ import Title from "../Title";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import EmailsList from "../EmailsList";
|
||||
import PriceList from "../PriceList";
|
||||
import { useApi } from "@/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
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 {
|
||||
const { t, i18n } = useTranslation();
|
||||
const locale = i18n.language;
|
||||
const api = useApi();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const homeConfig = useSelector(selectors.selectHome);
|
||||
const selectedPrice = useSelector(selectors.selectSelectedPrice);
|
||||
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
|
||||
const email = useSelector(selectors.selectEmail);
|
||||
const { products, getText } = usePaywall({
|
||||
placementKey: EPlacementKeys["aura.placement.main"],
|
||||
});
|
||||
const [countUsers, setCountUsers] = useState(752);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { sub_plans } = await api.getSubscriptionPlans({ locale });
|
||||
const plans = sub_plans
|
||||
.filter((plan: ISubscriptionPlan) => plan.provider === "stripe")
|
||||
.sort((a, b) => {
|
||||
if (!a.trial || !b.trial) {
|
||||
return 0;
|
||||
}
|
||||
if (a.trial?.price_cents < b.trial?.price_cents) {
|
||||
return -1;
|
||||
}
|
||||
if (a.trial?.price_cents > b.trial?.price_cents) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
setSubPlans(plans);
|
||||
})();
|
||||
}, [api, locale]);
|
||||
const randomDelay = getRandomArbitrary(3000, 5000);
|
||||
const countUsersTimeOut = setTimeout(() => {
|
||||
setCountUsers((prevState) => prevState + 1);
|
||||
}, randomDelay);
|
||||
return () => clearTimeout(countUsersTimeOut);
|
||||
}, [countUsers]);
|
||||
|
||||
const handleNext = () => {
|
||||
dispatch(
|
||||
@ -60,25 +49,33 @@ function PriceListPage(): JSX.Element {
|
||||
<>
|
||||
<UserHeader email={email} />
|
||||
<section className={`${styles.page} page`}>
|
||||
{!!subPlans.length && (
|
||||
{!!products.length && (
|
||||
<>
|
||||
<Title className={styles.title} variant="h2">
|
||||
{t("choose_your_own_fee")}
|
||||
</Title>
|
||||
<p className={styles.slogan}>{t("aura.web.price_selection")}</p>
|
||||
<div className={styles["emails-list-container"]}>
|
||||
<EmailsList />
|
||||
<EmailsList
|
||||
title={getText("text.5", {
|
||||
replacementSelector: "strong",
|
||||
replacement: {
|
||||
target: "${quantity}",
|
||||
replacement: countUsers.toString(),
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["price-list-container"]}>
|
||||
<PriceList
|
||||
activeItem={selectedPrice}
|
||||
subPlans={subPlans}
|
||||
products={products}
|
||||
click={handleNext}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!subPlans.length && <Loader />}
|
||||
{!products.length && <Loader />}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -5,14 +5,14 @@ import {
|
||||
useElements,
|
||||
} from "@stripe/react-stripe-js";
|
||||
import { PaymentRequest } from "@stripe/stripe-js";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import styles from "./styles.module.css";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import routes from "@/routes";
|
||||
import { IPaywallProduct } from "@/api/resources/Paywall";
|
||||
|
||||
interface ApplePayButtonProps {
|
||||
activeSubPlan: ISubscriptionPlan | null;
|
||||
activeProduct: IPaywallProduct | null;
|
||||
client_secret: string;
|
||||
subscriptionReceiptId?: string;
|
||||
returnUrl?: string;
|
||||
@ -20,7 +20,7 @@ interface ApplePayButtonProps {
|
||||
}
|
||||
|
||||
function ApplePayButton({
|
||||
activeSubPlan,
|
||||
activeProduct,
|
||||
client_secret,
|
||||
subscriptionReceiptId,
|
||||
returnUrl,
|
||||
@ -34,15 +34,15 @@ function ApplePayButton({
|
||||
null
|
||||
);
|
||||
|
||||
const getAmountFromSubPlan = (subPlan: ISubscriptionPlan) => {
|
||||
if (subPlan.trial) {
|
||||
return subPlan.trial.price_cents;
|
||||
const getAmountFromProduct = (subPlan: IPaywallProduct) => {
|
||||
if (subPlan.isTrial) {
|
||||
return subPlan.trialPrice;
|
||||
}
|
||||
return subPlan.price_cents;
|
||||
return subPlan.price;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!stripe || !elements || !activeSubPlan) {
|
||||
if (!stripe || !elements || !activeProduct) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -50,8 +50,8 @@ function ApplePayButton({
|
||||
country: "US",
|
||||
currency: "usd",
|
||||
total: {
|
||||
label: activeSubPlan.name || "Subscription",
|
||||
amount: getAmountFromSubPlan(activeSubPlan),
|
||||
label: activeProduct.name || "Subscription",
|
||||
amount: getAmountFromProduct(activeProduct),
|
||||
},
|
||||
requestPayerName: true,
|
||||
requestPayerEmail: true,
|
||||
@ -95,7 +95,6 @@ function ApplePayButton({
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
activeSubPlan,
|
||||
client_secret,
|
||||
dispatch,
|
||||
elements,
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { useApi } from "@/api";
|
||||
import Loader from "@/components/Loader";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Stripe, loadStripe } from "@stripe/stripe-js";
|
||||
import { Elements } from "@stripe/react-stripe-js";
|
||||
import CheckoutForm from "../PaymentPage/methods/Stripe/CheckoutForm";
|
||||
import { useAuth } from "@/auth";
|
||||
import styles from "./styles.module.css";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectors } from "@/store";
|
||||
@ -12,58 +10,63 @@ import { useNavigate } from "react-router-dom";
|
||||
import routes from "@/routes";
|
||||
import SubPlanInformation from "../SubPlanInformation";
|
||||
import Title from "../Title";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ApplePayButton from "./ApplePayButton";
|
||||
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
||||
import { EPlacementKeys } from "@/api/resources/Paywall";
|
||||
import { useMakePayment } from "@/hooks/payment/useMakePayment";
|
||||
|
||||
export function StripePage(): JSX.Element {
|
||||
const { i18n } = useTranslation();
|
||||
const api = useApi();
|
||||
const { token } = useAuth();
|
||||
const locale = i18n.language;
|
||||
const navigate = useNavigate();
|
||||
const activeSubPlan = useSelector(selectors.selectActiveSubPlan);
|
||||
const activeProduct = useSelector(selectors.selectActiveProduct);
|
||||
const email = useSelector(selectors.selectUser).email;
|
||||
const [stripePromise, setStripePromise] =
|
||||
useState<Promise<Stripe | null> | null>(null);
|
||||
const [clientSecret, setClientSecret] = useState<string>("");
|
||||
const [subscriptionReceiptId, setSubscriptionReceiptId] =
|
||||
useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
if (!activeSubPlan) {
|
||||
if (!activeProduct) {
|
||||
navigate(routes.client.priceList());
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const siteConfig = await api.getAppConfig({ bundleId: "auraweb" });
|
||||
setStripePromise(loadStripe(siteConfig.data.stripe_public_key));
|
||||
const { sub_plans } = await api.getSubscriptionPlans({ locale });
|
||||
const isActiveSubPlan = sub_plans.find(
|
||||
(subPlan) => subPlan.id === activeSubPlan?.id
|
||||
);
|
||||
if (!activeSubPlan || !isActiveSubPlan) {
|
||||
navigate(routes.client.priceList());
|
||||
}
|
||||
})();
|
||||
}, [activeSubPlan, api, locale, navigate]);
|
||||
const {
|
||||
paymentIntentId,
|
||||
clientSecret,
|
||||
returnUrl: checkoutUrl,
|
||||
paymentType,
|
||||
publicKey,
|
||||
isLoading,
|
||||
error,
|
||||
} = useMakePayment({
|
||||
productId: activeProduct?._id || "",
|
||||
});
|
||||
|
||||
if (checkoutUrl?.length) {
|
||||
window.location.href = checkoutUrl;
|
||||
}
|
||||
|
||||
const { products } = usePaywall({
|
||||
placementKey: EPlacementKeys["aura.placement.main"],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { subscription_receipt } = await api.createSubscriptionReceipt({
|
||||
token,
|
||||
way: "stripe",
|
||||
subscription_receipt: {
|
||||
sub_plan_id: activeSubPlan?.id || "stripe.7",
|
||||
},
|
||||
});
|
||||
const { id } = subscription_receipt;
|
||||
const { client_secret } = subscription_receipt.data;
|
||||
setSubscriptionReceiptId(id);
|
||||
setClientSecret(client_secret);
|
||||
setIsLoading(false);
|
||||
if (!products?.length || !publicKey) return;
|
||||
setStripePromise(loadStripe(publicKey));
|
||||
const isActiveProduct = products.find(
|
||||
(product) => product._id === activeProduct?._id
|
||||
);
|
||||
if (!activeProduct || !isActiveProduct) {
|
||||
navigate(routes.client.priceList());
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [api, token]);
|
||||
}, [activeProduct, navigate, products, publicKey]);
|
||||
|
||||
if (error?.length) {
|
||||
return (
|
||||
<div className={styles["payment-modal"]}>
|
||||
<Title variant="h3" className={styles.title}>
|
||||
Something went wrong
|
||||
</Title>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.page} page`}>
|
||||
@ -80,17 +83,18 @@ export function StripePage(): JSX.Element {
|
||||
<p className={styles.email}>{email}</p>
|
||||
</>
|
||||
)}
|
||||
{stripePromise && clientSecret && subscriptionReceiptId && (
|
||||
{stripePromise && clientSecret && paymentIntentId && (
|
||||
<Elements stripe={stripePromise} options={{ clientSecret }}>
|
||||
<ApplePayButton
|
||||
activeSubPlan={activeSubPlan}
|
||||
activeProduct={activeProduct}
|
||||
client_secret={clientSecret}
|
||||
subscriptionReceiptId={subscriptionReceiptId}
|
||||
subscriptionReceiptId={paymentIntentId}
|
||||
/>
|
||||
{activeProduct && <SubPlanInformation product={activeProduct} />}
|
||||
<CheckoutForm
|
||||
confirmType={paymentType}
|
||||
subscriptionReceiptId={paymentIntentId}
|
||||
/>
|
||||
{activeSubPlan && (
|
||||
<SubPlanInformation subPlan={activeSubPlan} />
|
||||
)}
|
||||
<CheckoutForm subscriptionReceiptId={subscriptionReceiptId} />
|
||||
</Elements>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,34 +1,34 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styles from "./styles.module.css";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import TotalToday from "./TotalToday";
|
||||
import ApplePayButton from "../StripePage/ApplePayButton";
|
||||
import { IPaywallProduct } from "@/api/resources/Paywall";
|
||||
|
||||
interface ISubPlanInformationProps {
|
||||
subPlan: ISubscriptionPlan;
|
||||
product: IPaywallProduct;
|
||||
client_secret?: string;
|
||||
}
|
||||
|
||||
const getPrice = (plan: ISubscriptionPlan): string => {
|
||||
const getPrice = (product: IPaywallProduct): string => {
|
||||
return `$${
|
||||
(plan.trial?.price_cents === 100 ? 99 : plan.trial?.price_cents || 0) / 100
|
||||
(product.trialPrice === 100 ? 99 : product.trialPrice || 0) / 100
|
||||
}`;
|
||||
};
|
||||
|
||||
function SubPlanInformation({
|
||||
subPlan,
|
||||
product,
|
||||
client_secret,
|
||||
}: ISubPlanInformationProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<TotalToday total={getPrice(subPlan)} />
|
||||
<TotalToday total={getPrice(product)} />
|
||||
{client_secret && (
|
||||
<ApplePayButton activeSubPlan={subPlan} client_secret={client_secret} />
|
||||
<ApplePayButton activeProduct={product} client_secret={client_secret} />
|
||||
)}
|
||||
<p className={styles.description}>
|
||||
{t("auweb.pay.information").replaceAll("%@", getPrice(subPlan))}.
|
||||
{t("auweb.pay.information").replaceAll("%@", getPrice(product))}.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -11,22 +11,23 @@ import styles from "./styles.module.css";
|
||||
// import Header from "../Header";
|
||||
// import SpecialWelcomeOffer from "../SpecialWelcomeOffer";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ISubscriptionPlan, ITrial } from "@/api/resources/SubscriptionPlans";
|
||||
import { ApiError, extractErrorMessage, useApi } from "@/api";
|
||||
import { useAuth } from "@/auth";
|
||||
import { getClientLocale, getClientTimezone } from "@/locales";
|
||||
import Loader from "../Loader";
|
||||
import Title from "../Title";
|
||||
import ErrorText from "../ErrorText";
|
||||
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
|
||||
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
||||
|
||||
const currency = Currency.USD;
|
||||
const locale = getClientLocale() as Locale;
|
||||
|
||||
const getPriceFromTrial = (trial: ITrial | null) => {
|
||||
if (!trial) {
|
||||
const getPrice = (product: IPaywallProduct | null) => {
|
||||
if (!product?.trialPrice) {
|
||||
return 0;
|
||||
}
|
||||
return (trial.price_cents === 100 ? 99 : trial.price_cents || 0) / 100;
|
||||
return (product.trialPrice === 100 ? 99 : product.trialPrice || 0) / 100;
|
||||
};
|
||||
|
||||
function SubscriptionPage(): JSX.Element {
|
||||
@ -48,37 +49,40 @@ function SubscriptionPage(): JSX.Element {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [apiError, setApiError] = useState<ApiError | null>(null);
|
||||
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 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(() => {
|
||||
if (subPlan) {
|
||||
const targetSubPlan = subPlans.find(
|
||||
(sub_plan) =>
|
||||
const targetProduct = products.find(
|
||||
(product) =>
|
||||
String(
|
||||
sub_plan?.trial?.price_cents
|
||||
? Math.floor((sub_plan?.trial?.price_cents + 1) / 100)
|
||||
: sub_plan.id.replace(".", "")
|
||||
product?.trialPrice
|
||||
? Math.floor((product?.trialPrice + 1) / 100)
|
||||
: product.key.replace(".", "")
|
||||
) === subPlan
|
||||
);
|
||||
if (targetSubPlan) {
|
||||
setActiveSubPlan(targetSubPlan);
|
||||
if (targetProduct) {
|
||||
setActiveProduct(targetProduct);
|
||||
}
|
||||
}
|
||||
}, [subPlan, subPlans]);
|
||||
}, [products, subPlan]);
|
||||
|
||||
const paymentItems = [
|
||||
{
|
||||
title: activeSubPlan?.name || "Per 7-Day Trial For",
|
||||
price: getPriceFromTrial(activeSubPlan?.trial || null),
|
||||
description: activeSubPlan?.desc.length
|
||||
? activeSubPlan?.desc
|
||||
title: activeProduct?.name || "Per 7-Day Trial For",
|
||||
price: getPrice(activeProduct),
|
||||
description: activeProduct?.description?.length
|
||||
? activeProduct.description
|
||||
: t("au.2week_plan.web"),
|
||||
},
|
||||
];
|
||||
@ -111,7 +115,7 @@ function SubscriptionPage(): JSX.Element {
|
||||
dispatch(actions.status.update("registred"));
|
||||
dispatch(
|
||||
actions.payment.update({
|
||||
activeSubPlan,
|
||||
activeProduct,
|
||||
})
|
||||
);
|
||||
setIsLoading(false);
|
||||
@ -176,29 +180,6 @@ function SubscriptionPage(): JSX.Element {
|
||||
setName(name);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { sub_plans } = await api.getSubscriptionPlans({ locale });
|
||||
const plans = sub_plans
|
||||
.filter(
|
||||
(plan: ISubscriptionPlan) => plan.provider === "stripe"
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (!a.trial || !b.trial) {
|
||||
return 0;
|
||||
}
|
||||
if (a?.trial?.price_cents < b?.trial?.price_cents) {
|
||||
return -1;
|
||||
}
|
||||
if (a?.trial?.price_cents > b?.trial?.price_cents) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
setSubPlans(plans);
|
||||
})();
|
||||
}, [api]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <SpecialWelcomeOffer open={isOpenModal} onClose={handleClick} /> */}
|
||||
@ -274,7 +255,7 @@ function SubscriptionPage(): JSX.Element {
|
||||
</div>
|
||||
<div className={styles["subscription-action"]}>
|
||||
<MainButton onClick={handleClick}>
|
||||
Start ${getPriceFromTrial(activeSubPlan?.trial || null)}
|
||||
Start ${getPrice(activeProduct || null)}
|
||||
</MainButton>
|
||||
</div>
|
||||
<Policy>
|
||||
|
||||
@ -2,38 +2,37 @@ import { useState } from "react";
|
||||
import styles from "./styles.module.css";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { actions } from "@/store";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import PriceItem from "../PriceItem";
|
||||
import { IPaywallProduct } from "@/api/resources/Paywall";
|
||||
|
||||
interface PriceListProps {
|
||||
subPlans: ISubscriptionPlan[];
|
||||
products: IPaywallProduct[];
|
||||
activeItem: number | null;
|
||||
classNameItem?: string;
|
||||
classNameItemActive?: string;
|
||||
click: () => void;
|
||||
}
|
||||
|
||||
const getPrice = (plan: ISubscriptionPlan) => {
|
||||
return (plan.trial?.price_cents || 0) / 100;
|
||||
const getPrice = (product: IPaywallProduct) => {
|
||||
return (product.trialPrice || 0) / 100;
|
||||
};
|
||||
|
||||
function PriceList({
|
||||
click,
|
||||
subPlans,
|
||||
products,
|
||||
classNameItem = "",
|
||||
classNameItemActive = "",
|
||||
}: PriceListProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const [activePlanItem, setActivePlanItem] =
|
||||
useState<ISubscriptionPlan | null>(null);
|
||||
const [activeProductItem, setActiveProductItem] = useState<IPaywallProduct>();
|
||||
|
||||
const priceItemClick = (id: string) => {
|
||||
const activePlan = subPlans.find((item) => item.id === String(id)) || null;
|
||||
setActivePlanItem(activePlan);
|
||||
if (activePlan) {
|
||||
const activeProduct = products.find((item) => item._id === String(id));
|
||||
setActiveProductItem(activeProduct);
|
||||
if (activeProduct) {
|
||||
dispatch(
|
||||
actions.payment.update({
|
||||
activeSubPlan: activePlan,
|
||||
activeProduct,
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -42,12 +41,12 @@ function PriceList({
|
||||
|
||||
return (
|
||||
<div className={`${styles.container}`}>
|
||||
{subPlans.map((plan, idx) => (
|
||||
{products.map((product, idx) => (
|
||||
<PriceItem
|
||||
active={plan.id === activePlanItem?.id}
|
||||
active={product._id === activeProductItem?._id}
|
||||
key={idx}
|
||||
value={getPrice(plan)}
|
||||
id={plan.id}
|
||||
value={getPrice(product)}
|
||||
id={product._id}
|
||||
className={classNameItem}
|
||||
classNameActive={classNameItemActive}
|
||||
click={priceItemClick}
|
||||
|
||||
@ -4,10 +4,8 @@ import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { actions, selectors } from "@/store";
|
||||
import { useApi } from "@/api";
|
||||
import routes from "@/routes";
|
||||
import NameInput from "./NameInput";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import Title from "@/components/Title";
|
||||
import EmailInput from "./EmailInput";
|
||||
import Policy from "@/components/Policy";
|
||||
@ -18,6 +16,8 @@ import { useDynamicSize } from "@/hooks/useDynamicSize";
|
||||
import QuestionnaireGreenButton from "../../ui/GreenButton";
|
||||
import { ESourceAuthorization } from "@/api/resources/User";
|
||||
import { useAuthentication } from "@/hooks/authentication/use-authentication";
|
||||
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
||||
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
|
||||
|
||||
interface IEmailEnterPage {
|
||||
redirectUrl?: string;
|
||||
@ -28,8 +28,7 @@ function EmailEnterPage({
|
||||
redirectUrl = routes.client.emailConfirmV1(),
|
||||
isRequiredName = false,
|
||||
}: IEmailEnterPage): JSX.Element {
|
||||
const api = useApi();
|
||||
const { t, i18n } = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const [email, setEmail] = useState("");
|
||||
@ -38,53 +37,33 @@ function EmailEnterPage({
|
||||
const [isValidEmail, setIsValidEmail] = useState(false);
|
||||
const [isValidName, setIsValidName] = useState(!isRequiredName);
|
||||
const [isAuth, setIsAuth] = useState(false);
|
||||
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
|
||||
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan);
|
||||
const [activeSubPlan, setActiveSubPlan] = useState<ISubscriptionPlan | null>(
|
||||
activeSubPlanFromStore
|
||||
);
|
||||
const locale = i18n.language;
|
||||
const { subPlan } = useParams();
|
||||
const { width: pageWidth, elementRef: pageRef } = useDynamicSize({});
|
||||
const { error, isLoading, authorization } = useAuthentication();
|
||||
const { gender } = useSelector(selectors.selectQuestionnaire);
|
||||
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
|
||||
const { products } = usePaywall({
|
||||
placementKey: EPlacementKeys["aura.placement.redesign.main"],
|
||||
});
|
||||
const [activeProduct, setActiveProduct] = useState<IPaywallProduct | null>(
|
||||
activeProductFromStore
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (subPlan) {
|
||||
const targetSubPlan = subPlans.find(
|
||||
(sub_plan) =>
|
||||
const targetProduct = products.find(
|
||||
(product) =>
|
||||
String(
|
||||
sub_plan?.trial?.price_cents
|
||||
? Math.floor((sub_plan?.trial?.price_cents + 1) / 100)
|
||||
: sub_plan.id.replace(".", "")
|
||||
product?.trialPrice
|
||||
? Math.floor((product?.trialPrice + 1) / 100)
|
||||
: product.key.replace(".", "")
|
||||
) === subPlan
|
||||
);
|
||||
if (targetSubPlan) {
|
||||
setActiveSubPlan(targetSubPlan);
|
||||
if (targetProduct) {
|
||||
setActiveProduct(targetProduct);
|
||||
}
|
||||
}
|
||||
}, [subPlan, subPlans]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { sub_plans } = await api.getSubscriptionPlans({ locale });
|
||||
const plans = sub_plans
|
||||
.filter((plan: ISubscriptionPlan) => plan.provider === "stripe")
|
||||
.sort((a, b) => {
|
||||
if (!a.trial || !b.trial) {
|
||||
return 0;
|
||||
}
|
||||
if (a?.trial?.price_cents < b?.trial?.price_cents) {
|
||||
return -1;
|
||||
}
|
||||
if (a?.trial?.price_cents > b?.trial?.price_cents) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
setSubPlans(plans);
|
||||
})();
|
||||
}, [api, locale]);
|
||||
}, [subPlan, products]);
|
||||
|
||||
const handleValidEmail = (email: string) => {
|
||||
dispatch(actions.form.addEmail(email));
|
||||
@ -127,7 +106,7 @@ function EmailEnterPage({
|
||||
await authorization(email, source);
|
||||
dispatch(
|
||||
actions.payment.update({
|
||||
activeSubPlan,
|
||||
activeProduct,
|
||||
})
|
||||
);
|
||||
setIsAuth(true);
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
import styles from "./styles.module.css";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useApi } from "@/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { actions, selectors } from "@/store";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@ -13,66 +10,32 @@ import BackgroundTopBlob from "../../ui/BackgroundTopBlob";
|
||||
import { useDynamicSize } from "@/hooks/useDynamicSize";
|
||||
import PriceList from "../../components/PriceList";
|
||||
import QuestionnaireGreenButton from "../../ui/GreenButton";
|
||||
|
||||
interface IPlanKey {
|
||||
[key: string]: number;
|
||||
}
|
||||
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
||||
import { EPlacementKeys } from "@/api/resources/Paywall";
|
||||
import { getRandomArbitrary } from "@/services/random-value";
|
||||
import Loader from "@/components/Loader";
|
||||
|
||||
function TrialChoicePage() {
|
||||
const { i18n } = useTranslation();
|
||||
const locale = i18n.language;
|
||||
const api = useApi();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const selectedPrice = useSelector(selectors.selectSelectedPrice);
|
||||
const homeConfig = useSelector(selectors.selectHome);
|
||||
const email = useSelector(selectors.selectEmail);
|
||||
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
|
||||
const [isDisabled, setIsDisabled] = useState(true);
|
||||
const allowedPlans = useMemo(() => [""], []);
|
||||
const [countUsers, setCountUsers] = useState(752);
|
||||
const { width: pageWidth, elementRef: pageRef } = useDynamicSize({});
|
||||
const { gender } = useSelector(selectors.selectQuestionnaire);
|
||||
const { products, isLoading, getText } = usePaywall({
|
||||
placementKey: EPlacementKeys["aura.placement.redesign.main"],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { sub_plans } = await api.getSubscriptionPlans({ locale });
|
||||
const plansWithoutTest = sub_plans.filter(
|
||||
(plan: ISubscriptionPlan) => !plan.name.includes("(test)")
|
||||
);
|
||||
const plansKeys: IPlanKey = {};
|
||||
const plans: ISubscriptionPlan[] = [];
|
||||
for (const plan of plansWithoutTest) {
|
||||
plansKeys[plan.name] = plansKeys[plan.name]
|
||||
? plansKeys[plan.name] + 1
|
||||
: 1;
|
||||
if (
|
||||
(plansKeys[plan.name] > 1 && !plan.trial?.is_free && !!plan.trial) ||
|
||||
allowedPlans.includes(plan.id)
|
||||
) {
|
||||
const targetPlan = plansWithoutTest.find(
|
||||
(item) => item.name === plan.name && item.id.includes("stripe")
|
||||
);
|
||||
plans.push(targetPlan as ISubscriptionPlan);
|
||||
}
|
||||
}
|
||||
|
||||
plans.sort((a, b) => {
|
||||
if (!a.trial || !b.trial) {
|
||||
return 0;
|
||||
}
|
||||
if (a.trial?.price_cents < b.trial?.price_cents) {
|
||||
return -1;
|
||||
}
|
||||
if (a.trial?.price_cents > b.trial?.price_cents) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
setSubPlans(plans);
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [api, locale]);
|
||||
const randomDelay = getRandomArbitrary(3000, 5000);
|
||||
const countUsersTimeOut = setTimeout(() => {
|
||||
setCountUsers((prevState) => prevState + 1);
|
||||
}, randomDelay);
|
||||
return () => clearTimeout(countUsersTimeOut);
|
||||
}, [countUsers]);
|
||||
|
||||
const handlePriceItem = () => {
|
||||
setIsDisabled(false);
|
||||
@ -101,60 +64,76 @@ function TrialChoicePage() {
|
||||
height={180}
|
||||
/>
|
||||
<Header className={styles.header} />
|
||||
<p className={styles.text} style={{ marginTop: "60px" }}>
|
||||
We've helped{" "}
|
||||
<span className={styles.blue}>
|
||||
<b>millions</b>
|
||||
</span>{" "}
|
||||
of people to have happier lives and better relationships, and we want to
|
||||
help you too.
|
||||
</p>
|
||||
<p className={`${styles.text} ${styles.bold}`}>
|
||||
Money shouldn’t stand in the way of finding astrology guidance that
|
||||
finally works. So, choose an amount that you think is reasonable to try
|
||||
us out for one week.
|
||||
</p>
|
||||
<p className={`${styles.text} ${styles.bold} ${styles.blue}`}>
|
||||
It costs us $13.67 to offer a 3-day trial, but please choose the amount
|
||||
you are comfortable with.
|
||||
</p>
|
||||
<div className={styles["price-container"]}>
|
||||
<PriceList
|
||||
subPlans={subPlans}
|
||||
activeItem={selectedPrice}
|
||||
classNameItem={styles["price-item"]}
|
||||
classNameItemActive={`${styles["price-item-active"]} ${styles[gender]}`}
|
||||
click={handlePriceItem}
|
||||
/>
|
||||
<p className={styles["auxiliary-text"]} style={{ maxWidth: "75%" }}>
|
||||
This option will help us support those who need to select the lowest
|
||||
trial prices!
|
||||
</p>
|
||||
<img
|
||||
className={styles["arrow-image"]}
|
||||
src="/arrow.svg"
|
||||
alt={`Arrow to $${subPlans.at(-1)}`}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["emails-list-container"]}>
|
||||
<EmailsList
|
||||
classNameContainer={`${styles["emails-container"]} ${styles[gender]}`}
|
||||
classNameTitle={styles["emails-title"]}
|
||||
classNameEmailItem={styles["email-item"]}
|
||||
direction="right-left"
|
||||
/>
|
||||
</div>
|
||||
<p className={styles.email}>{email}</p>
|
||||
<QuestionnaireGreenButton
|
||||
className={styles.button}
|
||||
disabled={isDisabled}
|
||||
onClick={handleNext}
|
||||
>
|
||||
See my plan
|
||||
</QuestionnaireGreenButton>
|
||||
<p className={styles["auxiliary-text"]}>
|
||||
*Cost of trial as of February 2024
|
||||
</p>
|
||||
{!isLoading && (
|
||||
<>
|
||||
<p className={styles.text} style={{ marginTop: "60px" }}>
|
||||
{getText("text.0", {
|
||||
replacementSelector: "b",
|
||||
color: "#1C38EA",
|
||||
})}
|
||||
</p>
|
||||
<p className={`${styles.text} ${styles.bold}`}>
|
||||
{getText("text.1", {
|
||||
color: "#1C38EA",
|
||||
})}
|
||||
</p>
|
||||
<p className={`${styles.text} ${styles.bold} ${styles.blue}`}>
|
||||
{getText("text.2", {
|
||||
color: "#1C38EA",
|
||||
})}
|
||||
</p>
|
||||
<div className={styles["price-container"]}>
|
||||
<PriceList
|
||||
products={products}
|
||||
activeItem={selectedPrice}
|
||||
classNameItem={styles["price-item"]}
|
||||
classNameItemActive={`${styles["price-item-active"]} ${styles[gender]}`}
|
||||
click={handlePriceItem}
|
||||
/>
|
||||
<p className={styles["auxiliary-text"]} style={{ maxWidth: "75%" }}>
|
||||
{getText("text.3", {
|
||||
color: "#1C38EA",
|
||||
})}
|
||||
</p>
|
||||
<img
|
||||
className={styles["arrow-image"]}
|
||||
src="/arrow.svg"
|
||||
alt={`Arrow to $${products.at(-1)?.trialPrice}`}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["emails-list-container"]}>
|
||||
<EmailsList
|
||||
title={getText("text.5", {
|
||||
replacementSelector: "strong",
|
||||
replacement: {
|
||||
target: "${quantity}",
|
||||
replacement: countUsers.toString(),
|
||||
},
|
||||
})}
|
||||
classNameContainer={`${styles["emails-container"]} ${styles[gender]}`}
|
||||
classNameTitle={styles["emails-title"]}
|
||||
classNameEmailItem={styles["email-item"]}
|
||||
direction="right-left"
|
||||
/>
|
||||
</div>
|
||||
<p className={styles.email}>{email}</p>
|
||||
<QuestionnaireGreenButton
|
||||
className={styles.button}
|
||||
disabled={isDisabled}
|
||||
onClick={handleNext}
|
||||
>
|
||||
{getText("text.button.1", {
|
||||
color: "#1C38EA",
|
||||
})}
|
||||
</QuestionnaireGreenButton>
|
||||
<p className={styles["auxiliary-text"]}>
|
||||
{getText("text.4", {
|
||||
color: "#1C38EA",
|
||||
})}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{isLoading && <Loader className={styles.loader} />}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@ -137,4 +137,11 @@
|
||||
|
||||
.email {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loader {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
@ -9,48 +9,56 @@ import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm";
|
||||
import { Stripe, loadStripe } from "@stripe/stripe-js";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectors } from "@/store";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import { useApi } from "@/api";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import routes from "@/routes";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAuth } from "@/auth";
|
||||
import Loader from "@/components/Loader";
|
||||
import { getPriceFromTrial } from "@/services/price";
|
||||
import SecurityPayments from "../SecurityPayments";
|
||||
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
|
||||
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
||||
import { useMakePayment } from "@/hooks/payment/useMakePayment";
|
||||
|
||||
interface IPaymentModalProps {
|
||||
activeSubscriptionPlan?: ISubscriptionPlan;
|
||||
activeProduct?: IPaywallProduct;
|
||||
noTrial?: boolean;
|
||||
returnUrl?: string;
|
||||
placementKey?: EPlacementKeys;
|
||||
}
|
||||
|
||||
const getPrice = (product: IPaywallProduct) => {
|
||||
return (product.trialPrice || 0) / 100;
|
||||
};
|
||||
|
||||
function PaymentModal({
|
||||
activeSubscriptionPlan,
|
||||
activeProduct,
|
||||
noTrial,
|
||||
returnUrl,
|
||||
placementKey = EPlacementKeys["aura.placement.redesign.main"],
|
||||
}: IPaymentModalProps) {
|
||||
const { i18n } = useTranslation();
|
||||
const locale = i18n.language;
|
||||
const api = useApi();
|
||||
const { token } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan);
|
||||
const activeSubPlan = activeSubscriptionPlan
|
||||
? activeSubscriptionPlan
|
||||
: activeSubPlanFromStore;
|
||||
const [stripePromise, setStripePromise] =
|
||||
useState<Promise<Stripe | null> | null>(null);
|
||||
const [clientSecret, setClientSecret] = useState<string>("");
|
||||
const [subscriptionReceiptId, setSubscriptionReceiptId] =
|
||||
useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isError, setIsError] = useState<boolean>(false);
|
||||
|
||||
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
|
||||
const _activeProduct = activeProduct ? activeProduct : activeProductFromStore;
|
||||
const {
|
||||
paymentIntentId,
|
||||
clientSecret,
|
||||
returnUrl: checkoutUrl,
|
||||
paymentType,
|
||||
publicKey,
|
||||
isLoading,
|
||||
error,
|
||||
} = useMakePayment({
|
||||
productId: _activeProduct?._id || "",
|
||||
returnPaidUrl:
|
||||
returnUrl
|
||||
});
|
||||
|
||||
if (checkoutUrl?.length) {
|
||||
window.location.href = checkoutUrl;
|
||||
}
|
||||
|
||||
const paymentMethodsButtons = useMemo(() => {
|
||||
// return paymentMethods.filter(
|
||||
// (method) => method.id !== EPaymentMethod.PAYMENT_BUTTONS
|
||||
// );
|
||||
return paymentMethods;
|
||||
}, []);
|
||||
|
||||
@ -58,49 +66,24 @@ function PaymentModal({
|
||||
EPaymentMethod.PAYMENT_BUTTONS
|
||||
);
|
||||
|
||||
const { products } = usePaywall({ placementKey });
|
||||
|
||||
const onSelectPaymentMethod = (method: EPaymentMethod) => {
|
||||
setSelectedPaymentMethod(method);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const siteConfig = await api.getAppConfig({ bundleId: "auraweb" });
|
||||
setStripePromise(loadStripe(siteConfig.data.stripe_public_key));
|
||||
const { sub_plans } = await api.getSubscriptionPlans({ locale });
|
||||
const isActiveSubPlan = sub_plans.find(
|
||||
(subPlan) => subPlan.id === activeSubPlan?.id
|
||||
if (!products?.length || !publicKey) return;
|
||||
setStripePromise(loadStripe(publicKey));
|
||||
const isActiveProduct = products.find(
|
||||
(product) => product._id === _activeProduct?._id
|
||||
);
|
||||
if (!activeSubPlan || !isActiveSubPlan) {
|
||||
navigate(routes.client.priceList());
|
||||
if (!_activeProduct || !isActiveProduct) {
|
||||
navigate(routes.client.trialChoiceV1());
|
||||
}
|
||||
})();
|
||||
}, [activeSubPlan, api, locale, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const { subscription_receipt } = await api.createSubscriptionReceipt({
|
||||
token,
|
||||
way: "stripe",
|
||||
subscription_receipt: {
|
||||
sub_plan_id: activeSubPlan?.id || "stripe.7",
|
||||
},
|
||||
});
|
||||
const { id } = subscription_receipt;
|
||||
const { client_secret } = subscription_receipt.data;
|
||||
const { checkout_url } = subscription_receipt.data;
|
||||
if (checkout_url?.length) {
|
||||
window.location.href = checkout_url;
|
||||
}
|
||||
setSubscriptionReceiptId(id);
|
||||
setClientSecret(client_secret);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setIsError(true);
|
||||
}
|
||||
})();
|
||||
}, [activeSubPlan?.id, api, token]);
|
||||
}, [_activeProduct, navigate, products, publicKey]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@ -112,7 +95,7 @@ function PaymentModal({
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
if (error?.length) {
|
||||
return (
|
||||
<div className={styles["payment-modal"]}>
|
||||
<Title variant="h3" className={styles.title}>
|
||||
@ -132,16 +115,13 @@ function PaymentModal({
|
||||
selectedPaymentMethod={selectedPaymentMethod}
|
||||
onSelectPaymentMethod={onSelectPaymentMethod}
|
||||
/>
|
||||
{activeSubPlan && (
|
||||
{_activeProduct && (
|
||||
<div>
|
||||
{!noTrial && (
|
||||
<>
|
||||
<p className={styles["sub-plan-description"]}>
|
||||
You will be charged only{" "}
|
||||
<b>
|
||||
${getPriceFromTrial(activeSubPlan?.trial)} for your 3-day
|
||||
trial.
|
||||
</b>
|
||||
<b>${getPrice(_activeProduct)} for your 3-day trial.</b>
|
||||
</p>
|
||||
<p className={styles["sub-plan-description"]}>
|
||||
We`ll <b>email you a reminder</b> before your trial period ends.
|
||||
@ -160,9 +140,9 @@ function PaymentModal({
|
||||
{selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && (
|
||||
<div className={styles["payment-method"]}>
|
||||
<ApplePayButton
|
||||
activeSubPlan={activeSubPlan}
|
||||
activeProduct={_activeProduct}
|
||||
client_secret={clientSecret}
|
||||
subscriptionReceiptId={subscriptionReceiptId}
|
||||
subscriptionReceiptId={paymentIntentId}
|
||||
returnUrl={window.location.href}
|
||||
/>
|
||||
</div>
|
||||
@ -170,7 +150,8 @@ function PaymentModal({
|
||||
|
||||
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
|
||||
<CheckoutForm
|
||||
subscriptionReceiptId={subscriptionReceiptId}
|
||||
confirmType={paymentType}
|
||||
subscriptionReceiptId={paymentIntentId}
|
||||
returnUrl={returnUrl}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -1,19 +1,22 @@
|
||||
import Title from "@/components/Title";
|
||||
import styles from "./styles.module.css";
|
||||
import { getPriceFromTrial } from "@/services/price";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import CustomButton from "../CustomButton";
|
||||
import GuardPayments from "../GuardPayments";
|
||||
import { useState } from "react";
|
||||
import FullScreenModal from "@/components/FullScreenModal";
|
||||
import { IPaywallProduct } from "@/api/resources/Paywall";
|
||||
|
||||
interface IPaymentTableProps {
|
||||
subPlan: ISubscriptionPlan;
|
||||
product: IPaywallProduct;
|
||||
gender: string;
|
||||
buttonClick: () => void;
|
||||
}
|
||||
|
||||
function PaymentTable({ gender, subPlan, buttonClick }: IPaymentTableProps) {
|
||||
const getPrice = (product: IPaywallProduct) => {
|
||||
return (product.trialPrice || 0) / 100;
|
||||
};
|
||||
|
||||
function PaymentTable({ gender, product, buttonClick }: IPaymentTableProps) {
|
||||
const [isOpenPrivacyModal, setIsOpenPrivacyModal] = useState<boolean>(false);
|
||||
const handleSubscriptionPolicyClick = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
@ -50,20 +53,18 @@ function PaymentTable({ gender, subPlan, buttonClick }: IPaymentTableProps) {
|
||||
<div className={styles["table-container"]}>
|
||||
<Title variant="h3" className={styles.title}>
|
||||
Personalized reading for{" "}
|
||||
<span className={styles.purple}>
|
||||
${getPriceFromTrial(subPlan?.trial)}
|
||||
</span>
|
||||
<span className={styles.purple}>${getPrice(product)}</span>
|
||||
</Title>
|
||||
<div className={styles["table-element"]}>
|
||||
<p className={styles["total-today"]}>Total today:</p>
|
||||
<span>${getPriceFromTrial(subPlan?.trial)}</span>
|
||||
<span>${getPrice(product)}</span>
|
||||
</div>
|
||||
<hr />
|
||||
<div className={styles["table-element"]}>
|
||||
<p>Your cost per 2 weeks after trial</p>
|
||||
<div>
|
||||
<span className={styles.discount}>$65</span>
|
||||
<span>${subPlan.price_cents / 100}</span>
|
||||
<span>${product.trialPrice / 100}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -75,9 +76,9 @@ function PaymentTable({ gender, subPlan, buttonClick }: IPaymentTableProps) {
|
||||
<p className={styles.policy}>
|
||||
You are enrolling in 2 weeks subscription. By continuing you agree that
|
||||
if you don't cancel prior to the end of the 3-day trial for the $
|
||||
{getPriceFromTrial(subPlan?.trial)} you will automatically be charged
|
||||
$19 every 2 weeks until you cancel in settings. Learn more about
|
||||
cancellation and refund policy in{" "}
|
||||
{getPrice(product)} you will automatically be charged $19 every 2 weeks
|
||||
until you cancel in settings. Learn more about cancellation and refund
|
||||
policy in{" "}
|
||||
<a onClick={handleSubscriptionPolicyClick}>Subscription policy</a>
|
||||
</p>
|
||||
</>
|
||||
|
||||
@ -12,11 +12,7 @@ import YourReading from "./components/YourReading";
|
||||
import Reviews from "./components/Reviews";
|
||||
import PointsList from "./components/PointsList";
|
||||
import OftenAsk from "./components/OftenAsk";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useApi } from "@/api";
|
||||
import { getClientLocale } from "@/locales";
|
||||
import { Locale } from "@/components/PaymentTable";
|
||||
import WithPartnerInformation from "./components/WithPartnerInformation";
|
||||
import Modal from "@/components/Modal";
|
||||
import PaymentModal from "./components/PaymentModal";
|
||||
@ -26,12 +22,11 @@ import TrialPaymentHeader from "./components/Header";
|
||||
import Header from "../../components/Header";
|
||||
import BackgroundTopBlob from "../../ui/BackgroundTopBlob";
|
||||
import { useDynamicSize } from "@/hooks/useDynamicSize";
|
||||
|
||||
const locale = getClientLocale() as Locale;
|
||||
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
|
||||
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
||||
|
||||
function TrialPaymentPage() {
|
||||
const dispatch = useDispatch();
|
||||
const api = useApi();
|
||||
const navigate = useNavigate();
|
||||
const birthdate = useSelector(selectors.selectBirthdate);
|
||||
const zodiacSign = getZodiacSignByDate(birthdate);
|
||||
@ -46,10 +41,12 @@ function TrialPaymentPage() {
|
||||
flowChoice,
|
||||
} = useSelector(selectors.selectQuestionnaire);
|
||||
const partnerZodiacSign = getZodiacSignByDate(partnerBirthdate);
|
||||
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
|
||||
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan);
|
||||
const [activeSubPlan, setActiveSubPlan] = useState<ISubscriptionPlan | null>(
|
||||
activeSubPlanFromStore
|
||||
const { products } = usePaywall({
|
||||
placementKey: EPlacementKeys["aura.placement.redesign.main"],
|
||||
});
|
||||
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
|
||||
const [activeProduct, setActiveProduct] = useState<IPaywallProduct | null>(
|
||||
activeProductFromStore
|
||||
);
|
||||
const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false);
|
||||
const [singleOrWithPartner, setSingleOrWithPartner] = useState<
|
||||
@ -57,43 +54,22 @@ function TrialPaymentPage() {
|
||||
>("single");
|
||||
const { subPlan } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { sub_plans } = await api.getSubscriptionPlans({ locale });
|
||||
const plans = sub_plans
|
||||
.filter((plan: ISubscriptionPlan) => plan.provider === "stripe")
|
||||
.sort((a, b) => {
|
||||
if (!a.trial || !b.trial) {
|
||||
return 0;
|
||||
}
|
||||
if (a?.trial?.price_cents < b?.trial?.price_cents) {
|
||||
return -1;
|
||||
}
|
||||
if (a?.trial?.price_cents > b?.trial?.price_cents) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
setSubPlans(plans);
|
||||
})();
|
||||
}, [api]);
|
||||
|
||||
useEffect(() => {
|
||||
if (subPlan) {
|
||||
const targetSubPlan = subPlans.find(
|
||||
(sub_plan) =>
|
||||
const targetProduct = products.find(
|
||||
(product) =>
|
||||
String(
|
||||
sub_plan?.trial?.price_cents
|
||||
? Math.floor((sub_plan?.trial?.price_cents + 1) / 100)
|
||||
: sub_plan.id.replace(".", "")
|
||||
product?.trialPrice
|
||||
? Math.floor((product?.trialPrice + 1) / 100)
|
||||
: product.key.replace(".", "")
|
||||
) === subPlan
|
||||
);
|
||||
if (targetSubPlan) {
|
||||
setActiveSubPlan(targetSubPlan);
|
||||
dispatch(actions.payment.update({ activeSubPlan: targetSubPlan }));
|
||||
if (targetProduct) {
|
||||
setActiveProduct(targetProduct);
|
||||
dispatch(actions.payment.update({ activeProduct }));
|
||||
}
|
||||
}
|
||||
}, [dispatch, subPlan, subPlans]);
|
||||
}, [dispatch, subPlan, products, activeProduct]);
|
||||
|
||||
useEffect(() => {
|
||||
if (["relationship", "married"].includes(flowChoice)) {
|
||||
@ -103,7 +79,7 @@ function TrialPaymentPage() {
|
||||
setSingleOrWithPartner("single");
|
||||
}, [flowChoice]);
|
||||
|
||||
if (!activeSubPlan) {
|
||||
if (!activeProduct) {
|
||||
return <Navigate to={routes.client.trialChoice()} />;
|
||||
}
|
||||
|
||||
@ -168,7 +144,7 @@ function TrialPaymentPage() {
|
||||
<Goal goal={goal} />
|
||||
<PaymentTable
|
||||
gender={gender}
|
||||
subPlan={activeSubPlan}
|
||||
product={activeProduct}
|
||||
buttonClick={openStripeModal}
|
||||
/>
|
||||
<YourReading
|
||||
@ -185,7 +161,7 @@ function TrialPaymentPage() {
|
||||
<OftenAsk />
|
||||
<PaymentTable
|
||||
gender={gender}
|
||||
subPlan={activeSubPlan}
|
||||
product={activeProduct}
|
||||
buttonClick={openStripeModal}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@ -2,10 +2,17 @@ import Title from "@/components/Title";
|
||||
import styles from "./styles.module.css";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectors } from "@/store";
|
||||
import { getPriceFromTrial } from "@/services/price";
|
||||
import { IPaywallProduct } from "@/api/resources/Paywall";
|
||||
|
||||
const getPrice = (product: IPaywallProduct | null) => {
|
||||
if (!product) {
|
||||
return 0;
|
||||
}
|
||||
return (product.trialPrice === 100 ? 99 : product.trialPrice || 0) / 100;
|
||||
};
|
||||
|
||||
function PaymentDiscountTable() {
|
||||
const activeSub = useSelector(selectors.selectActiveSubPlan);
|
||||
const activeProduct = useSelector(selectors.selectActiveProduct);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
@ -15,7 +22,11 @@ function PaymentDiscountTable() {
|
||||
<p className={styles["no-pressure"]}>No pressure. Cancel anytime.</p>
|
||||
<div className={styles.applied}>
|
||||
<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>
|
||||
</div>
|
||||
<div className={styles.side}>
|
||||
@ -34,7 +45,7 @@ function PaymentDiscountTable() {
|
||||
<hr className={styles.line} />
|
||||
<div className={styles["total-container"]}>
|
||||
<p>Total today:</p>
|
||||
{activeSub && <strong>${getPriceFromTrial(activeSub.trial)}</strong>}
|
||||
{activeProduct && <strong>${getPrice(activeProduct)}</strong>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -5,29 +5,31 @@ import MainButton from "@/components/MainButton";
|
||||
import Modal from "@/components/Modal";
|
||||
import PaymentModal from "../../TrialPayment/components/PaymentModal";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import { useApi } from "@/api";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
||||
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
|
||||
|
||||
function MarketingTrialPayment() {
|
||||
const { i18n } = useTranslation();
|
||||
const locale = i18n.language;
|
||||
const api = useApi();
|
||||
const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false);
|
||||
const [freeTrialPlan, setFreeTrialPlan] = useState<
|
||||
ISubscriptionPlan | undefined
|
||||
const [freeTrialProduct, setFreeTrialProduct] = useState<
|
||||
IPaywallProduct | undefined
|
||||
>();
|
||||
|
||||
const { products } = usePaywall({
|
||||
placementKey: EPlacementKeys["aura.placement.main"],
|
||||
});
|
||||
|
||||
// get free trial plan
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { sub_plans } = await api.getSubscriptionPlans({ locale });
|
||||
const _freeTrialPlan = sub_plans.find(
|
||||
(subPlan) => subPlan.trial?.is_free
|
||||
);
|
||||
setFreeTrialPlan(_freeTrialPlan);
|
||||
const _freeProduct = products.find((product) => product.isFreeTrial);
|
||||
setFreeTrialProduct(_freeProduct);
|
||||
})();
|
||||
}, [api, locale]);
|
||||
}, [api, locale, products]);
|
||||
|
||||
const openStripeModal = () => {
|
||||
setIsOpenPaymentModal(true);
|
||||
@ -44,7 +46,7 @@ function MarketingTrialPayment() {
|
||||
open={isOpenPaymentModal}
|
||||
onClose={handleCloseModal}
|
||||
>
|
||||
<PaymentModal activeSubscriptionPlan={freeTrialPlan} />
|
||||
<PaymentModal activeProduct={freeTrialProduct} />
|
||||
</Modal>
|
||||
<section className={`${styles.page} page`}>
|
||||
<div className={styles.wrapper}>
|
||||
|
||||
@ -9,9 +9,15 @@ interface IPaymentFormProps {
|
||||
stripePublicKey: string;
|
||||
clientSecret: string;
|
||||
returnUrl: string;
|
||||
confirmType?: "payment" | "setup";
|
||||
}
|
||||
|
||||
function PaymentForm({ stripePublicKey, clientSecret, returnUrl }: IPaymentFormProps) {
|
||||
function PaymentForm({
|
||||
stripePublicKey,
|
||||
clientSecret,
|
||||
returnUrl,
|
||||
confirmType = "payment",
|
||||
}: IPaymentFormProps) {
|
||||
const [stripePromise, setStripePromise] =
|
||||
useState<Promise<Stripe | null> | null>(null);
|
||||
|
||||
@ -23,7 +29,7 @@ function PaymentForm({ stripePublicKey, clientSecret, returnUrl }: IPaymentFormP
|
||||
<div className={styles["payment-method-container"]}>
|
||||
{stripePromise && clientSecret && (
|
||||
<Elements stripe={stripePromise} options={{ clientSecret }}>
|
||||
<CheckoutForm returnUrl={returnUrl} />
|
||||
<CheckoutForm confirmType={confirmType} returnUrl={returnUrl} />
|
||||
</Elements>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,73 +1,36 @@
|
||||
import PriceList from "@/components/PriceList";
|
||||
import styles from "./styles.module.css";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useApi } from "@/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { actions, selectors } from "@/store";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import routes from "@/routes";
|
||||
import EmailsList from "@/components/EmailsList";
|
||||
import MainButton from "@/components/MainButton";
|
||||
|
||||
interface IPlanKey {
|
||||
[key: string]: number;
|
||||
}
|
||||
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
||||
import { EPlacementKeys } from "@/api/resources/Paywall";
|
||||
import { getRandomArbitrary } from "@/services/random-value";
|
||||
|
||||
function TrialChoicePage() {
|
||||
const { i18n } = useTranslation();
|
||||
const locale = i18n.language;
|
||||
const api = useApi();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const selectedPrice = useSelector(selectors.selectSelectedPrice);
|
||||
const homeConfig = useSelector(selectors.selectHome);
|
||||
const email = useSelector(selectors.selectEmail);
|
||||
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
|
||||
const [isDisabled, setIsDisabled] = useState(true);
|
||||
const allowedPlans = useMemo(() => [""], []);
|
||||
const [countUsers, setCountUsers] = useState(752);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { sub_plans } = await api.getSubscriptionPlans({ locale });
|
||||
const plansWithoutTest = sub_plans.filter(
|
||||
(plan: ISubscriptionPlan) => !plan.name.includes("(test)")
|
||||
);
|
||||
const plansKeys: IPlanKey = {};
|
||||
const plans: ISubscriptionPlan[] = [];
|
||||
for (const plan of plansWithoutTest) {
|
||||
plansKeys[plan.name] = plansKeys[plan.name]
|
||||
? plansKeys[plan.name] + 1
|
||||
: 1;
|
||||
if (
|
||||
(plansKeys[plan.name] > 1 && !plan.trial?.is_free && !!plan.trial) ||
|
||||
allowedPlans.includes(plan.id)
|
||||
) {
|
||||
const targetPlan = plansWithoutTest.find(
|
||||
(item) => item.name === plan.name && item.id.includes("stripe")
|
||||
);
|
||||
plans.push(targetPlan as ISubscriptionPlan);
|
||||
}
|
||||
}
|
||||
const randomDelay = getRandomArbitrary(3000, 5000);
|
||||
const countUsersTimeOut = setTimeout(() => {
|
||||
setCountUsers((prevState) => prevState + 1);
|
||||
}, randomDelay);
|
||||
return () => clearTimeout(countUsersTimeOut);
|
||||
}, [countUsers]);
|
||||
|
||||
plans.sort((a, b) => {
|
||||
if (!a.trial || !b.trial) {
|
||||
return 0;
|
||||
}
|
||||
if (a.trial?.price_cents < b.trial?.price_cents) {
|
||||
return -1;
|
||||
}
|
||||
if (a.trial?.price_cents > b.trial?.price_cents) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
setSubPlans(plans);
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [api, locale]);
|
||||
const { products, getText } = usePaywall({
|
||||
placementKey: EPlacementKeys["aura.placement.main"],
|
||||
});
|
||||
|
||||
const handlePriceItem = () => {
|
||||
setIsDisabled(false);
|
||||
@ -85,38 +48,49 @@ function TrialChoicePage() {
|
||||
return (
|
||||
<section className={`${styles.page} page`}>
|
||||
<p className={styles.text}>
|
||||
We've helped millions of people to have happier lives and better
|
||||
relationships, and we want to help you too.
|
||||
{getText("text.0", {
|
||||
replacementSelector: "b",
|
||||
color: "#1C38EA",
|
||||
})}
|
||||
</p>
|
||||
<p className={`${styles.text} ${styles.bold}`}>
|
||||
Money shouldn’t stand in the way of finding astrology guidance that
|
||||
finally works. So, choose an amount that you think is reasonable to try
|
||||
us out for one week.
|
||||
{getText("text.1", {
|
||||
color: "#1C38EA",
|
||||
})}
|
||||
</p>
|
||||
<p className={`${styles.text} ${styles.bold} ${styles.purple}`}>
|
||||
It costs us $13.67 to offer a 3-day trial, but please choose the amount
|
||||
you are comfortable with.
|
||||
{getText("text.2", {
|
||||
color: "#1C38EA",
|
||||
})}
|
||||
</p>
|
||||
<div className={styles["price-container"]}>
|
||||
<PriceList
|
||||
subPlans={subPlans}
|
||||
products={products}
|
||||
activeItem={selectedPrice}
|
||||
classNameItem={styles["price-item"]}
|
||||
classNameItemActive={styles["price-item-active"]}
|
||||
click={handlePriceItem}
|
||||
/>
|
||||
<p className={styles["auxiliary-text"]} style={{ maxWidth: "75%" }}>
|
||||
This option will help us support those who need to select the lowest
|
||||
trial prices!
|
||||
{getText("text.3", {
|
||||
color: "#1C38EA",
|
||||
})}
|
||||
</p>
|
||||
<img
|
||||
className={styles["arrow-image"]}
|
||||
src="/arrow.svg"
|
||||
alt={`Arrow to $${subPlans.at(-1)}`}
|
||||
alt={`Arrow to $${products.at(-1)?.name}`}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["emails-list-container"]}>
|
||||
<EmailsList
|
||||
title={getText("text.5", {
|
||||
replacementSelector: "strong",
|
||||
replacement: {
|
||||
target: "${quantity}",
|
||||
replacement: countUsers.toString(),
|
||||
},
|
||||
})}
|
||||
classNameContainer={styles["emails-container"]}
|
||||
classNameTitle={styles["emails-title"]}
|
||||
classNameEmailItem={styles["email-item"]}
|
||||
@ -129,10 +103,14 @@ function TrialChoicePage() {
|
||||
disabled={isDisabled}
|
||||
onClick={handleNext}
|
||||
>
|
||||
See my plan
|
||||
{getText("text.button.1", {
|
||||
color: "#1C38EA",
|
||||
})}
|
||||
</MainButton>
|
||||
<p className={styles["auxiliary-text"]}>
|
||||
*Cost of trial as of February 2024
|
||||
{getText("text.4", {
|
||||
color: "#1C38EA",
|
||||
})}
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
|
||||
@ -9,43 +9,58 @@ import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm";
|
||||
import { Stripe, loadStripe } from "@stripe/stripe-js";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectors } from "@/store";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import { useApi } from "@/api";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import routes from "@/routes";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAuth } from "@/auth";
|
||||
import Loader from "@/components/Loader";
|
||||
import { getPriceFromTrial } from "@/services/price";
|
||||
import SecurityPayments from "../SecurityPayments";
|
||||
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
|
||||
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
||||
import { useMakePayment } from "@/hooks/payment/useMakePayment";
|
||||
|
||||
interface IPaymentModalProps {
|
||||
activeSubscriptionPlan?: ISubscriptionPlan;
|
||||
activeProduct?: IPaywallProduct;
|
||||
noTrial?: boolean;
|
||||
returnUrl?: string;
|
||||
}
|
||||
|
||||
const getPrice = (product: IPaywallProduct | null) => {
|
||||
if (!product) {
|
||||
return 0;
|
||||
}
|
||||
return (product.trialPrice === 100 ? 99 : product.trialPrice || 0) / 100;
|
||||
};
|
||||
|
||||
function PaymentModal({
|
||||
activeSubscriptionPlan,
|
||||
activeProduct,
|
||||
noTrial,
|
||||
returnUrl,
|
||||
}: IPaymentModalProps) {
|
||||
const { i18n } = useTranslation();
|
||||
const locale = i18n.language;
|
||||
const api = useApi();
|
||||
const { token } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan);
|
||||
const activeSubPlan = activeSubscriptionPlan
|
||||
? activeSubscriptionPlan
|
||||
: activeSubPlanFromStore;
|
||||
const [stripePromise, setStripePromise] =
|
||||
useState<Promise<Stripe | null> | null>(null);
|
||||
const [clientSecret, setClientSecret] = useState<string>("");
|
||||
const [subscriptionReceiptId, setSubscriptionReceiptId] =
|
||||
useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isError, setIsError] = useState<boolean>(false);
|
||||
|
||||
const { products } = usePaywall({
|
||||
placementKey: EPlacementKeys["aura.placement.main"],
|
||||
});
|
||||
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
|
||||
|
||||
const _activeProduct = activeProduct ? activeProduct : activeProductFromStore;
|
||||
const {
|
||||
paymentIntentId,
|
||||
clientSecret,
|
||||
returnUrl: checkoutUrl,
|
||||
paymentType,
|
||||
publicKey,
|
||||
isLoading,
|
||||
error,
|
||||
} = useMakePayment({
|
||||
productId: _activeProduct?._id || "",
|
||||
returnPaidUrl: returnUrl,
|
||||
});
|
||||
|
||||
if (checkoutUrl?.length) {
|
||||
window.location.href = checkoutUrl;
|
||||
}
|
||||
|
||||
const paymentMethodsButtons = useMemo(() => {
|
||||
// return paymentMethods.filter(
|
||||
@ -64,43 +79,16 @@ function PaymentModal({
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const siteConfig = await api.getAppConfig({ bundleId: "auraweb" });
|
||||
setStripePromise(loadStripe(siteConfig.data.stripe_public_key));
|
||||
const { sub_plans } = await api.getSubscriptionPlans({ locale });
|
||||
const isActiveSubPlan = sub_plans.find(
|
||||
(subPlan) => subPlan.id === activeSubPlan?.id
|
||||
if (!products?.length || !publicKey) return;
|
||||
setStripePromise(loadStripe(publicKey));
|
||||
const isActiveProduct = products.find(
|
||||
(product) => product._id === _activeProduct?._id
|
||||
);
|
||||
if (!activeSubPlan || !isActiveSubPlan) {
|
||||
navigate(routes.client.priceList());
|
||||
if (!_activeProduct || !isActiveProduct) {
|
||||
navigate(routes.client.trialChoice());
|
||||
}
|
||||
})();
|
||||
}, [activeSubPlan, api, locale, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const { subscription_receipt } = await api.createSubscriptionReceipt({
|
||||
token,
|
||||
way: "stripe",
|
||||
subscription_receipt: {
|
||||
sub_plan_id: activeSubPlan?.id || "stripe.7",
|
||||
},
|
||||
});
|
||||
const { id } = subscription_receipt;
|
||||
const { client_secret } = subscription_receipt.data;
|
||||
const { checkout_url } = subscription_receipt.data;
|
||||
if (checkout_url?.length) {
|
||||
window.location.href = checkout_url;
|
||||
}
|
||||
setSubscriptionReceiptId(id);
|
||||
setClientSecret(client_secret);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setIsError(true);
|
||||
}
|
||||
})();
|
||||
}, [activeSubPlan?.id, api, token]);
|
||||
}, [_activeProduct, navigate, products, publicKey]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@ -112,7 +100,7 @@ function PaymentModal({
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
if (error?.length) {
|
||||
return (
|
||||
<div className={styles["payment-modal"]}>
|
||||
<Title variant="h3" className={styles.title}>
|
||||
@ -132,16 +120,13 @@ function PaymentModal({
|
||||
selectedPaymentMethod={selectedPaymentMethod}
|
||||
onSelectPaymentMethod={onSelectPaymentMethod}
|
||||
/>
|
||||
{activeSubPlan && (
|
||||
{_activeProduct && (
|
||||
<div>
|
||||
{!noTrial && (
|
||||
<>
|
||||
<p className={styles["sub-plan-description"]}>
|
||||
You will be charged only{" "}
|
||||
<b>
|
||||
${getPriceFromTrial(activeSubPlan?.trial)} for your 3-day
|
||||
trial.
|
||||
</b>
|
||||
<b>${getPrice(_activeProduct)} for your 3-day trial.</b>
|
||||
</p>
|
||||
<p className={styles["sub-plan-description"]}>
|
||||
We`ll <b>email you a reminder</b> before your trial period ends.
|
||||
@ -160,9 +145,9 @@ function PaymentModal({
|
||||
{selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && (
|
||||
<div className={styles["payment-method"]}>
|
||||
<ApplePayButton
|
||||
activeSubPlan={activeSubPlan}
|
||||
activeProduct={_activeProduct}
|
||||
client_secret={clientSecret}
|
||||
subscriptionReceiptId={subscriptionReceiptId}
|
||||
subscriptionReceiptId={paymentIntentId}
|
||||
returnUrl={window.location.href}
|
||||
/>
|
||||
</div>
|
||||
@ -170,7 +155,8 @@ function PaymentModal({
|
||||
|
||||
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
|
||||
<CheckoutForm
|
||||
subscriptionReceiptId={subscriptionReceiptId}
|
||||
confirmType={paymentType}
|
||||
subscriptionReceiptId={paymentIntentId}
|
||||
returnUrl={returnUrl}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -1,18 +1,21 @@
|
||||
import Title from "@/components/Title";
|
||||
import styles from "./styles.module.css";
|
||||
import { getPriceFromTrial } from "@/services/price";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import CustomButton from "../CustomButton";
|
||||
import GuardPayments from "../GuardPayments";
|
||||
import { useState } from "react";
|
||||
import FullScreenModal from "@/components/FullScreenModal";
|
||||
import { IPaywallProduct } from "@/api/resources/Paywall";
|
||||
|
||||
interface IPaymentTableProps {
|
||||
subPlan: ISubscriptionPlan;
|
||||
product: IPaywallProduct;
|
||||
buttonClick: () => void;
|
||||
}
|
||||
|
||||
function PaymentTable({ subPlan, buttonClick }: IPaymentTableProps) {
|
||||
const getPrice = (product: IPaywallProduct) => {
|
||||
return (product.trialPrice || 0) / 100;
|
||||
};
|
||||
|
||||
function PaymentTable({ product, buttonClick }: IPaymentTableProps) {
|
||||
const [isOpenPrivacyModal, setIsOpenPrivacyModal] = useState<boolean>(false);
|
||||
const handleSubscriptionPolicyClick = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
@ -44,20 +47,18 @@ function PaymentTable({ subPlan, buttonClick }: IPaymentTableProps) {
|
||||
<div className={styles["table-container"]}>
|
||||
<Title variant="h3" className={styles.title}>
|
||||
Personalized reading for{" "}
|
||||
<span className={styles.purple}>
|
||||
${getPriceFromTrial(subPlan?.trial)}
|
||||
</span>
|
||||
<span className={styles.purple}>${getPrice(product)}</span>
|
||||
</Title>
|
||||
<div className={styles["table-element"]}>
|
||||
<p>Total today:</p>
|
||||
<span>${getPriceFromTrial(subPlan?.trial)}</span>
|
||||
<span>${getPrice(product)}</span>
|
||||
</div>
|
||||
<hr />
|
||||
<div className={styles["table-element"]}>
|
||||
<p>Your cost per 2 weeks after trial</p>
|
||||
<div>
|
||||
<span className={styles.discount}>$65</span>
|
||||
<span>${subPlan.price_cents / 100}</span>
|
||||
<span>${product.trialPrice / 100}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -69,9 +70,9 @@ function PaymentTable({ subPlan, buttonClick }: IPaymentTableProps) {
|
||||
<p className={styles.policy}>
|
||||
You are enrolling in 2 weeks subscription. By continuing you agree that
|
||||
if you don't cancel prior to the end of the 3-day trial for the $
|
||||
{getPriceFromTrial(subPlan?.trial)} you will automatically be charged
|
||||
$19 every 2 weeks until you cancel in settings. Learn more about
|
||||
cancellation and refund policy in{" "}
|
||||
{getPrice(product)} you will automatically be charged $19 every 2 weeks
|
||||
until you cancel in settings. Learn more about cancellation and refund
|
||||
policy in{" "}
|
||||
<a onClick={handleSubscriptionPolicyClick}>Subscription policy</a>
|
||||
</p>
|
||||
</>
|
||||
|
||||
@ -13,22 +13,17 @@ import YourReading from "./components/YourReading";
|
||||
import Reviews from "./components/Reviews";
|
||||
import PointsList from "./components/PointsList";
|
||||
import OftenAsk from "./components/OftenAsk";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useApi } from "@/api";
|
||||
import { getClientLocale } from "@/locales";
|
||||
import { Locale } from "@/components/PaymentTable";
|
||||
import WithPartnerInformation from "./components/WithPartnerInformation";
|
||||
import Modal from "@/components/Modal";
|
||||
import PaymentModal from "./components/PaymentModal";
|
||||
import { trialPaymentPointsList } from "@/data/pointsLists";
|
||||
import { trialPaymentReviews } from "@/data/reviews";
|
||||
|
||||
const locale = getClientLocale() as Locale;
|
||||
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
||||
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
|
||||
|
||||
function TrialPaymentPage() {
|
||||
const dispatch = useDispatch();
|
||||
const api = useApi();
|
||||
const navigate = useNavigate();
|
||||
const birthdate = useSelector(selectors.selectBirthdate);
|
||||
const zodiacSign = getZodiacSignByDate(birthdate);
|
||||
@ -42,55 +37,36 @@ function TrialPaymentPage() {
|
||||
flowChoice,
|
||||
} = useSelector(selectors.selectQuestionnaire);
|
||||
const partnerZodiacSign = getZodiacSignByDate(partnerBirthdate);
|
||||
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
|
||||
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan);
|
||||
const [activeSubPlan, setActiveSubPlan] = useState<ISubscriptionPlan | null>(
|
||||
activeSubPlanFromStore
|
||||
);
|
||||
const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false);
|
||||
const [marginTopTitle, setMarginTopTitle] = useState<number>(360);
|
||||
const [singleOrWithPartner, setSingleOrWithPartner] = useState<
|
||||
"single" | "partner"
|
||||
>("single");
|
||||
const { subPlan } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { sub_plans } = await api.getSubscriptionPlans({ locale });
|
||||
const plans = sub_plans
|
||||
.filter((plan: ISubscriptionPlan) => plan.provider === "stripe")
|
||||
.sort((a, b) => {
|
||||
if (!a.trial || !b.trial) {
|
||||
return 0;
|
||||
}
|
||||
if (a?.trial?.price_cents < b?.trial?.price_cents) {
|
||||
return -1;
|
||||
}
|
||||
if (a?.trial?.price_cents > b?.trial?.price_cents) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
setSubPlans(plans);
|
||||
})();
|
||||
}, [api]);
|
||||
const { products } = usePaywall({
|
||||
placementKey: EPlacementKeys["aura.placement.main"],
|
||||
});
|
||||
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
|
||||
const [activeProduct, setActiveProduct] = useState<IPaywallProduct | null>(
|
||||
activeProductFromStore
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (subPlan) {
|
||||
const targetSubPlan = subPlans.find(
|
||||
(sub_plan) =>
|
||||
const targetProduct = products.find(
|
||||
(product) =>
|
||||
String(
|
||||
sub_plan?.trial?.price_cents
|
||||
? Math.floor((sub_plan?.trial?.price_cents + 1) / 100)
|
||||
: sub_plan.id.replace(".", "")
|
||||
product?.trialPrice
|
||||
? Math.floor((product?.trialPrice + 1) / 100)
|
||||
: product.key.replace(".", "")
|
||||
) === subPlan
|
||||
);
|
||||
if (targetSubPlan) {
|
||||
setActiveSubPlan(targetSubPlan);
|
||||
dispatch(actions.payment.update({ activeSubPlan: targetSubPlan }));
|
||||
if (targetProduct) {
|
||||
setActiveProduct(targetProduct);
|
||||
dispatch(actions.payment.update({ activeProduct }));
|
||||
}
|
||||
}
|
||||
}, [dispatch, subPlan, subPlans]);
|
||||
}, [dispatch, subPlan, products, activeProduct]);
|
||||
|
||||
useEffect(() => {
|
||||
if (["relationship", "married"].includes(flowChoice)) {
|
||||
@ -102,7 +78,7 @@ function TrialPaymentPage() {
|
||||
setMarginTopTitle(340);
|
||||
}, [flowChoice]);
|
||||
|
||||
if (!activeSubPlan) {
|
||||
if (!activeProduct) {
|
||||
return <Navigate to={routes.client.trialChoice()} />;
|
||||
}
|
||||
|
||||
@ -157,7 +133,7 @@ function TrialPaymentPage() {
|
||||
Your Personalized Clarity & Love Reading is ready!
|
||||
</Title>
|
||||
<Goal goal={goal} />
|
||||
<PaymentTable subPlan={activeSubPlan} buttonClick={openStripeModal} />
|
||||
<PaymentTable product={activeProduct} buttonClick={openStripeModal} />
|
||||
<YourReading
|
||||
gender={gender}
|
||||
zodiacSign={zodiacSign}
|
||||
@ -174,7 +150,7 @@ function TrialPaymentPage() {
|
||||
<Reviews reviews={trialPaymentReviews} />
|
||||
<PointsList title="What you get" points={trialPaymentPointsList} />
|
||||
<OftenAsk />
|
||||
<PaymentTable subPlan={activeSubPlan} buttonClick={openStripeModal} />
|
||||
<PaymentTable product={activeProduct} buttonClick={openStripeModal} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,10 +2,17 @@ import Title from "@/components/Title";
|
||||
import styles from "./styles.module.css";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectors } from "@/store";
|
||||
import { getPriceFromTrial } from "@/services/price";
|
||||
import { IPaywallProduct } from "@/api/resources/Paywall";
|
||||
|
||||
const getPrice = (product: IPaywallProduct | null) => {
|
||||
if (!product) {
|
||||
return 0;
|
||||
}
|
||||
return (product.trialPrice === 100 ? 99 : product.trialPrice || 0) / 100;
|
||||
};
|
||||
|
||||
function PaymentDiscountTable() {
|
||||
const activeSub = useSelector(selectors.selectActiveSubPlan);
|
||||
const activeProduct = useSelector(selectors.selectActiveProduct);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
@ -15,7 +22,11 @@ function PaymentDiscountTable() {
|
||||
<p className={styles["no-pressure"]}>No pressure. Cancel anytime.</p>
|
||||
<div className={styles.applied}>
|
||||
<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>
|
||||
</div>
|
||||
<div className={styles.side}>
|
||||
@ -34,7 +45,7 @@ function PaymentDiscountTable() {
|
||||
<hr className={styles.line} />
|
||||
<div className={styles["total-container"]}>
|
||||
<p>Total today:</p>
|
||||
{activeSub && <strong>${getPriceFromTrial(activeSub.trial)}</strong>}
|
||||
{activeProduct && <strong>${getPrice(activeProduct)}</strong>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -2,21 +2,21 @@ import React from "react";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import './payment-screen.css';
|
||||
import "./payment-screen.css";
|
||||
|
||||
import useSteps, { Step } from '@/hooks/palmistry/use-steps';
|
||||
import useTimer from '@/hooks/palmistry/use-timer';
|
||||
import HeaderLogo from '@/components/palmistry/header-logo/header-logo';
|
||||
import useSteps, { Step } from "@/hooks/palmistry/use-steps";
|
||||
import useTimer from "@/hooks/palmistry/use-timer";
|
||||
import HeaderLogo from "@/components/palmistry/header-logo/header-logo";
|
||||
import PaymentModal from "@/components/pages/TrialPayment/components/PaymentModal";
|
||||
import { selectors } from "@/store";
|
||||
|
||||
const getFormattedPrice = (price: number) => {
|
||||
return (price / 100).toFixed(2);
|
||||
}
|
||||
};
|
||||
|
||||
export default function PaymentScreen() {
|
||||
const time = useTimer();
|
||||
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan);
|
||||
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
|
||||
const subscriptionStatus = useSelector(selectors.selectStatus);
|
||||
|
||||
const steps = useSteps();
|
||||
@ -27,18 +27,20 @@ export default function PaymentScreen() {
|
||||
steps.goNext();
|
||||
}, 1500);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [subscriptionStatus]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!activeSubPlanFromStore) {
|
||||
if (!activeProductFromStore) {
|
||||
steps.setFirstUnpassedStep(Step.SubscriptionPlan);
|
||||
}
|
||||
}, [activeSubPlanFromStore]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeProductFromStore]);
|
||||
|
||||
const trialPrice = activeSubPlanFromStore?.trial?.price_cents || 0;
|
||||
const fullPrice = activeSubPlanFromStore?.price_cents || 0;
|
||||
const trialPrice = activeProductFromStore?.trialPrice || 0;
|
||||
const fullPrice = activeProductFromStore?.price || 0;
|
||||
|
||||
const [minutes, seconds] = time.split(':');
|
||||
const [minutes, seconds] = time.split(":");
|
||||
|
||||
return (
|
||||
<div className="payment-screen">
|
||||
@ -236,9 +238,17 @@ export default function PaymentScreen() {
|
||||
|
||||
<style>{`.palmistry-payment-modal { max-height: calc(100dvh - 40px) }`}</style>
|
||||
|
||||
{activeSubPlanFromStore && (
|
||||
<div className={`payment-screen__widget${subscriptionStatus === "subscribed" ? " payment-screen__widget_success" : ""}`}>
|
||||
{subscriptionStatus !== "subscribed" && <PaymentModal returnUrl={window.location.href}/>}
|
||||
{activeProductFromStore && (
|
||||
<div
|
||||
className={`payment-screen__widget${
|
||||
subscriptionStatus === "subscribed"
|
||||
? " payment-screen__widget_success"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{subscriptionStatus !== "subscribed" && (
|
||||
<PaymentModal returnUrl={window.location.href} />
|
||||
)}
|
||||
|
||||
{subscriptionStatus === "subscribed" && (
|
||||
<div className="payment-screen__success">
|
||||
@ -255,7 +265,9 @@ export default function PaymentScreen() {
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div className="payment-screen__success-text">Payment success</div>
|
||||
<div className="payment-screen__success-text">
|
||||
Payment success
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,92 +1,58 @@
|
||||
import React, { useMemo } from "react";
|
||||
import React from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { selectors } from "@/store";
|
||||
import useSteps, { Step } from "@/hooks/palmistry/use-steps";
|
||||
import Button from "@/components/palmistry/button/button";
|
||||
import EmailHeader from "@/components/palmistry/email-header/email-header";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import { actions } from "@/store";
|
||||
import { useApi } from "@/api";
|
||||
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
|
||||
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
||||
|
||||
const bestPlanId = "stripe.15";
|
||||
|
||||
const getFormattedPrice = (plan: ISubscriptionPlan) => {
|
||||
return (plan.trial!.price_cents / 100).toFixed(2);
|
||||
const getFormattedPrice = (product: IPaywallProduct) => {
|
||||
return (product?.trialPrice / 100).toFixed(2);
|
||||
};
|
||||
|
||||
export default function StepSubscriptionPlan() {
|
||||
const steps = useSteps();
|
||||
const dispatch = useDispatch();
|
||||
const api = useApi();
|
||||
const { i18n } = useTranslation();
|
||||
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan);
|
||||
const allowedPlans = useMemo(() => [""], []);
|
||||
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
|
||||
const { products } = usePaywall({
|
||||
placementKey: EPlacementKeys["aura.placement.main"],
|
||||
});
|
||||
|
||||
const storedEmail = steps.getStoredValue(Step.Email);
|
||||
|
||||
const [subscriptionPlan, setSubscriptionPlan] = React.useState("");
|
||||
const [subscriptionPlans, setSubscriptionPlans] = React.useState<
|
||||
ISubscriptionPlan[]
|
||||
>([]);
|
||||
const [product, setProduct] = React.useState("");
|
||||
const [email, setEmail] = React.useState(steps.getStoredValue(Step.Email));
|
||||
|
||||
const locale = i18n.language;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (activeSubPlanFromStore) {
|
||||
setSubscriptionPlan(activeSubPlanFromStore.id);
|
||||
if (activeProductFromStore) {
|
||||
setProduct(activeProductFromStore._id);
|
||||
}
|
||||
}, [activeSubPlanFromStore]);
|
||||
}, [activeProductFromStore]);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
const { sub_plans } = await api.getSubscriptionPlans({ locale });
|
||||
const plans = sub_plans
|
||||
.filter(
|
||||
(plan: ISubscriptionPlan) =>
|
||||
plan.provider === "stripe" && !plan.name.includes("(test)")
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (!a.trial || !b.trial) {
|
||||
return 0;
|
||||
}
|
||||
if (a?.trial?.price_cents < b?.trial?.price_cents) {
|
||||
return -1;
|
||||
}
|
||||
if (a?.trial?.price_cents > b?.trial?.price_cents) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
setSubscriptionPlans(
|
||||
plans.filter(
|
||||
(plan) => plan.trial?.price_cents || allowedPlans.includes(plan.id)
|
||||
)
|
||||
);
|
||||
})();
|
||||
}, [allowedPlans, api, locale]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (subscriptionPlan) {
|
||||
const targetSubPlan = subscriptionPlans.find(
|
||||
(sub_plan) => sub_plan.id === subscriptionPlan
|
||||
if (product) {
|
||||
const targetProduct = products.find(
|
||||
(_product) => _product._id === product
|
||||
);
|
||||
|
||||
if (targetSubPlan) {
|
||||
dispatch(actions.payment.update({ activeSubPlan: targetSubPlan }));
|
||||
if (targetProduct) {
|
||||
dispatch(actions.payment.update({ activeProduct: targetProduct }));
|
||||
}
|
||||
}
|
||||
}, [subscriptionPlan]);
|
||||
}, [dispatch, product, products]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setEmail(storedEmail || "");
|
||||
}, [storedEmail]);
|
||||
|
||||
const onNext = () => {
|
||||
steps.saveCurrent(subscriptionPlan);
|
||||
steps.saveCurrent(product);
|
||||
steps.goNext();
|
||||
};
|
||||
|
||||
@ -146,24 +112,22 @@ export default function StepSubscriptionPlan() {
|
||||
</div>
|
||||
|
||||
<div className="palmistry-container__plans">
|
||||
{subscriptionPlans.map((plan) => (
|
||||
{products.map((_product) => (
|
||||
<div
|
||||
key={plan.id}
|
||||
key={_product._id}
|
||||
className={`palmistry-container__plan ${
|
||||
subscriptionPlan === plan.id
|
||||
? "palmistry-container__plan_active"
|
||||
: ""
|
||||
product === _product._id ? "palmistry-container__plan_active" : ""
|
||||
}`}
|
||||
onClick={() => setSubscriptionPlan(plan.id)}
|
||||
onClick={() => setProduct(_product._id)}
|
||||
>
|
||||
<h3>${getFormattedPrice(plan)}</h3>
|
||||
<h3>${getFormattedPrice(_product)}</h3>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={`palmistry-container__subscription-text ${
|
||||
subscriptionPlan === bestPlanId
|
||||
product === bestPlanId
|
||||
? "palmistry-container__subscription-text_active"
|
||||
: ""
|
||||
}`}
|
||||
|
||||
@ -1,19 +1,20 @@
|
||||
import React from 'react';
|
||||
import { StripeError } from '@stripe/stripe-js';
|
||||
import React from "react";
|
||||
import { StripeError } from "@stripe/stripe-js";
|
||||
import {
|
||||
PaymentElement,
|
||||
useElements,
|
||||
useStripe,
|
||||
} from '@stripe/react-stripe-js';
|
||||
} from "@stripe/react-stripe-js";
|
||||
|
||||
import './stripe-form.css';
|
||||
import "./stripe-form.css";
|
||||
|
||||
import Button from '../button/button';
|
||||
import Button from "../button/button";
|
||||
|
||||
type Props = {
|
||||
subscriptionReceiptId: string;
|
||||
isProcessing: boolean;
|
||||
paymentResultUrl: string;
|
||||
confirmType: "payment" | "setup";
|
||||
onSubmit: () => void;
|
||||
onSuccess: () => void;
|
||||
onError: (error: StripeError) => void;
|
||||
@ -22,6 +23,7 @@ type Props = {
|
||||
export default function StripeForm(props: Props) {
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
const confirmType = props.confirmType || "payment";
|
||||
|
||||
const [formReady, setFormReady] = React.useState(false);
|
||||
|
||||
@ -33,7 +35,9 @@ export default function StripeForm(props: Props) {
|
||||
props.onSubmit();
|
||||
|
||||
try {
|
||||
const { error } = await stripe.confirmPayment({
|
||||
const { error } = await stripe[
|
||||
confirmType === "payment" ? "confirmPayment" : "confirmSetup"
|
||||
]({
|
||||
elements,
|
||||
confirmParams: {
|
||||
return_url: props.paymentResultUrl,
|
||||
@ -52,10 +56,14 @@ export default function StripeForm(props: Props) {
|
||||
|
||||
return (
|
||||
<form className="stripe-form" onSubmit={onSubmit}>
|
||||
<PaymentElement onReady={() => setFormReady(true)}/>
|
||||
<PaymentElement onReady={() => setFormReady(true)} />
|
||||
|
||||
<div className="stripe-form__button">
|
||||
<Button type="submit" disabled={props.isProcessing || !formReady} active>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={props.isProcessing || !formReady}
|
||||
active
|
||||
>
|
||||
Pay
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
1
src/env.d.ts
vendored
1
src/env.d.ts
vendored
@ -1,6 +1,7 @@
|
||||
interface ImportMetaEnv {
|
||||
AURA_API_HOST: string,
|
||||
AURA_DAPI_HOST: string,
|
||||
AURA_DAPI_PREFIX: string,
|
||||
AURA_SITE_HOST: string,
|
||||
AURA_PREFIX: string,
|
||||
AURA_OPEN_AI_HOST: number,
|
||||
|
||||
79
src/hooks/payment/useMakePayment.ts
Normal file
79
src/hooks/payment/useMakePayment.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { useApi } from "@/api";
|
||||
import { selectors } from "@/store";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
interface IUseMakePaymentProps {
|
||||
productId: string;
|
||||
returnPaidUrl?: string;
|
||||
}
|
||||
|
||||
export const useMakePayment = ({ productId, returnPaidUrl = `https://${window.location.host}/payment/result/` }: IUseMakePaymentProps) => {
|
||||
const api = useApi();
|
||||
const token = useSelector(selectors.selectToken);
|
||||
const [paymentIntentId, setPaymentIntentId] = useState<string>();
|
||||
const [paymentType, setPaymentType] = useState<"payment" | "setup">("payment");
|
||||
const [clientSecret, setClientSecret] = useState<string>();
|
||||
const [publicKey, setPublicKey] = useState<string>();
|
||||
const [returnUrl, setReturnUrl] = useState<string>();
|
||||
const [error, setError] = useState<string>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const makePayment = useCallback(async () => {
|
||||
if (!productId?.length) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await api.makePayment({
|
||||
token,
|
||||
productId
|
||||
});
|
||||
|
||||
if (res.status === "paid") {
|
||||
return window.location.href = `${returnPaidUrl}?redirect_status=succeeded`;
|
||||
}
|
||||
|
||||
if ("message" in res && res.message) {
|
||||
return setError(res.message);
|
||||
}
|
||||
|
||||
if (!("data" in res) || !res.data) {
|
||||
return;
|
||||
}
|
||||
const { data, type } = res;
|
||||
setPaymentIntentId(data.paymentIntentId);
|
||||
setPaymentType(type);
|
||||
setClientSecret(data.client_secret);
|
||||
setReturnUrl(data.return_url);
|
||||
setPublicKey(data.public_key);
|
||||
} catch (error) {
|
||||
setError(error as string);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [api, productId, returnPaidUrl, token])
|
||||
|
||||
useEffect(() => {
|
||||
makePayment()
|
||||
}, [makePayment])
|
||||
|
||||
|
||||
return useMemo(() => ({
|
||||
paymentIntentId,
|
||||
paymentType,
|
||||
clientSecret,
|
||||
returnUrl,
|
||||
publicKey,
|
||||
error,
|
||||
isLoading
|
||||
}), [
|
||||
clientSecret,
|
||||
error,
|
||||
isLoading,
|
||||
paymentIntentId,
|
||||
paymentType,
|
||||
publicKey,
|
||||
returnUrl
|
||||
])
|
||||
}
|
||||
103
src/hooks/paywall/usePaywall.tsx
Normal file
103
src/hooks/paywall/usePaywall.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import { useApi } from "@/api";
|
||||
import { EPlacementKeys, IPaywall } from "@/api/resources/Paywall";
|
||||
import { selectors } from "@/store";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import parse from "html-react-parser";
|
||||
|
||||
interface IUsePaywallProps {
|
||||
placementKey: EPlacementKeys;
|
||||
}
|
||||
|
||||
interface IGetTextProps {
|
||||
replacementSelector?: string;
|
||||
color?: string;
|
||||
replacement?: {
|
||||
target: string;
|
||||
replacement: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function usePaywall({ placementKey }: IUsePaywallProps) {
|
||||
const api = useApi();
|
||||
const token = useSelector(selectors.selectToken);
|
||||
const [paywall, setPaywall] = useState<IPaywall>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const products = useMemo(() => paywall?.products || [], [paywall?.products]);
|
||||
const properties = useMemo(
|
||||
() => paywall?.properties || [],
|
||||
[paywall?.properties]
|
||||
);
|
||||
|
||||
const getPaywallByPlacementKey = useCallback(
|
||||
async (placementKey: EPlacementKeys) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(false);
|
||||
const paywall = await api.getPaywallByPlacementKey({
|
||||
placementKey,
|
||||
token,
|
||||
});
|
||||
if ("paywall" in paywall && paywall.paywall) {
|
||||
setPaywall(paywall.paywall);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[api, token]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
getPaywallByPlacementKey(placementKey);
|
||||
}, [getPaywallByPlacementKey, placementKey]);
|
||||
|
||||
const getText = useCallback(
|
||||
(
|
||||
key: string,
|
||||
{
|
||||
replacementSelector = "span",
|
||||
color = "inherit",
|
||||
replacement,
|
||||
}: IGetTextProps
|
||||
) => {
|
||||
const property = properties.find((property) => property.key === key);
|
||||
if (!property) return "";
|
||||
const text = property.value;
|
||||
const colorElements = properties.filter((property) =>
|
||||
property.key.includes(`${key}.color`)
|
||||
);
|
||||
if (text && colorElements.length) {
|
||||
let element = text;
|
||||
for (const colorElement of colorElements) {
|
||||
element = element.replace(
|
||||
colorElement.value,
|
||||
`<${replacementSelector} class="${property.key}" style="color: ${color}">${colorElement.value}</${replacementSelector}>`
|
||||
);
|
||||
}
|
||||
return parse(element);
|
||||
}
|
||||
if (text && replacement) {
|
||||
return text.replace(replacement.target, replacement.replacement);
|
||||
}
|
||||
return text;
|
||||
},
|
||||
[properties]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
paywall,
|
||||
isLoading,
|
||||
error,
|
||||
products,
|
||||
properties,
|
||||
getText,
|
||||
}),
|
||||
[error, isLoading, paywall, products, properties, getText]
|
||||
);
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import { EPlacementKeys } from "./api/resources/Paywall";
|
||||
import type { UserStatus } from "./types";
|
||||
|
||||
const environments = import.meta.env
|
||||
@ -5,6 +6,7 @@ const environments = import.meta.env
|
||||
const host = "";
|
||||
export const apiHost = environments.AURA_API_HOST;
|
||||
const dApiHost = environments.AURA_DAPI_HOST
|
||||
const dApiPrefix = environments.AURA_DAPI_PREFIX
|
||||
const siteHost = environments.AURA_SITE_HOST
|
||||
const prefix = environments.AURA_PREFIX;
|
||||
const openAIHost = environments.AURA_OPEN_AI_HOST;
|
||||
@ -262,6 +264,15 @@ const routes = {
|
||||
// Palmistry
|
||||
getPalmistryLines: () =>
|
||||
["https://api.aura.witapps.us", "palmistry", "lines"].join("/"),
|
||||
|
||||
// Paywall
|
||||
getPaywallByPlacementKey: (placementKey: EPlacementKeys) =>
|
||||
[dApiHost, dApiPrefix, "placement", placementKey, "paywall"].join("/"),
|
||||
|
||||
// Payment
|
||||
makePayment: () =>
|
||||
[dApiHost, dApiPrefix, "payment", "checkout"].join("/"),
|
||||
|
||||
},
|
||||
openAi: {
|
||||
createThread: () => [openAIHost, openAiPrefix, "threads"].join("/"),
|
||||
|
||||
@ -29,7 +29,7 @@ import onboardingConfig, {
|
||||
} from "./onboarding";
|
||||
import payment, {
|
||||
actions as paymentActions,
|
||||
selectActiveSubPlan,
|
||||
selectActiveProduct,
|
||||
selectIsDiscount,
|
||||
selectSubscriptionReceipt,
|
||||
} from "./payment";
|
||||
@ -97,7 +97,7 @@ export const selectors = {
|
||||
selectSelfName,
|
||||
selectCategoryId,
|
||||
selectSelectedPrice,
|
||||
selectActiveSubPlan,
|
||||
selectActiveProduct,
|
||||
selectUserCallbacksDescription,
|
||||
selectUserCallbacksPrevStat,
|
||||
selectHome,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import { IPaywallProduct } from "@/api/resources/Paywall";
|
||||
import { SubscriptionReceipt } from "@/api/resources/UserSubscriptionReceipts";
|
||||
import { createSlice, createSelector } from "@reduxjs/toolkit";
|
||||
import type { PayloadAction } from "@reduxjs/toolkit";
|
||||
@ -6,15 +6,15 @@ import type { PayloadAction } from "@reduxjs/toolkit";
|
||||
interface IPayment {
|
||||
selectedPrice: number | null;
|
||||
isDiscount: boolean;
|
||||
activeSubPlan: ISubscriptionPlan | null;
|
||||
subscriptionReceipt: SubscriptionReceipt | null;
|
||||
activeProduct: IPaywallProduct | null;
|
||||
}
|
||||
|
||||
const initialState: IPayment = {
|
||||
selectedPrice: null,
|
||||
isDiscount: false,
|
||||
activeSubPlan: null,
|
||||
subscriptionReceipt: null,
|
||||
activeProduct: null,
|
||||
};
|
||||
|
||||
const paymentSlice = createSlice({
|
||||
@ -33,8 +33,8 @@ export const selectSelectedPrice = createSelector(
|
||||
(state: { payment: IPayment }) => state.payment.selectedPrice,
|
||||
(payment) => payment
|
||||
);
|
||||
export const selectActiveSubPlan = createSelector(
|
||||
(state: { payment: IPayment }) => state.payment.activeSubPlan,
|
||||
export const selectActiveProduct = createSelector(
|
||||
(state: { payment: IPayment }) => state.payment.activeProduct,
|
||||
(payment) => payment
|
||||
);
|
||||
export const selectIsDiscount = createSelector(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user