Preview/discount pages

This commit is contained in:
Денис Катаев 2024-02-10 02:30:02 +00:00 committed by Victor Ershov
parent 5df6445233
commit 1ccc5aa835
39 changed files with 1158 additions and 44 deletions

BIN
public/applepay.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

15
public/credit-card.svg Normal file
View File

@ -0,0 +1,15 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg"
class="sc-89953eaa-8 fvUjAM">
<g clip-path="url(#clip0_7128_73411)">
<path
d="M21.5 4.5H3.5C2.39543 4.5 1.5 5.39543 1.5 6.5V18.5C1.5 19.6046 2.39543 20.5 3.5 20.5H21.5C22.6046 20.5 23.5 19.6046 23.5 18.5V6.5C23.5 5.39543 22.6046 4.5 21.5 4.5Z"
stroke="#63639d" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M1.5 10.5H23.5" stroke="#63639d" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
</path>
</g>
<defs>
<clipPath id="clip0_7128_73411">
<rect width="24" height="24" fill="#63639d" transform="translate(0.5 0.5)"></rect>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 793 B

BIN
public/ellipse.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
public/fire.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
public/friends.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
public/paypal.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
public/present.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

6
public/security.svg Normal file
View File

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
class="sc-d6893fb9-4 ealxSu">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M21.9032 4.36098C22.0287 4.41968 22.1406 4.50347 22.232 4.6071C22.4222 4.8205 22.52 5.09985 22.5041 5.38445C22.0405 14.7267 18.6677 20.5084 12.7479 23.8686C12.593 23.9553 12.4222 24 12.2524 24C12.0825 24 11.9117 23.9553 11.7578 23.8686C5.83795 20.5084 2.46417 14.7267 2.00151 5.38445C1.98647 5.09981 2.08453 4.82067 2.2746 4.6071C2.36629 4.50361 2.47841 4.41994 2.60401 4.36125C2.7296 4.30257 2.86599 4.27014 3.00472 4.26596C4.50945 4.22688 6.29315 3.09865 7.846 2.11642C8.5158 1.69275 9.14266 1.29625 9.68565 1.02604C10.7977 0.473312 11.5325 0.108869 12.0844 0.012864C12.1963 -0.0039698 12.3102 -0.00428381 12.4222 0.011932C12.974 0.107936 13.7088 0.47238 14.8219 1.02511C15.3644 1.29535 15.9906 1.69156 16.6597 2.11493C18.2129 3.09774 19.9975 4.22687 21.5028 4.26596C21.6414 4.26993 21.7777 4.30228 21.9032 4.36098ZM15.1657 8.76305H15.7482C16.0572 8.76305 16.3536 8.89071 16.5721 9.11795C16.7906 9.34519 16.9133 9.65339 16.9133 9.97476V16.0333C16.9133 16.3547 16.7906 16.6629 16.5721 16.8901C16.3536 17.1173 16.0572 17.245 15.7482 17.245H8.7576C8.44859 17.245 8.15224 17.1173 7.93374 16.8901C7.71525 16.6629 7.59249 16.3547 7.59249 16.0333V9.97476C7.59249 9.65339 7.71525 9.34519 7.93374 9.11795C8.15224 8.89071 8.44859 8.76305 8.7576 8.76305H9.34015V8.1572C9.34015 6.48686 10.6468 5.12793 12.2529 5.12793C13.859 5.12793 15.1657 6.48686 15.1657 8.1572V8.76305ZM12.2529 6.33964C11.2894 6.33964 10.5053 7.15512 10.5053 8.1572V8.76305H14.0006V8.1572C14.0006 7.15512 13.2164 6.33964 12.2529 6.33964ZM12.8355 14.8216V13.4421C13.1821 13.2318 13.418 12.8453 13.418 12.3982C13.418 12.0768 13.2953 11.7686 13.0768 11.5414C12.8583 11.3141 12.5619 11.1865 12.2529 11.1865C11.9439 11.1865 11.6476 11.3141 11.4291 11.5414C11.2106 11.7686 11.0878 12.0768 11.0878 12.3982C11.0878 12.8459 11.3237 13.2324 11.6704 13.4421V14.8216H12.8355Z"
fill="#27AE60"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -91,8 +91,14 @@ import OnboardingPage from "../pages/Onboarding";
import TrialChoicePage from "../pages/TrialChoice";
import TrialPaymentPage from "../pages/TrialPayment";
import ReactGA from "react-ga4";
import AdditionalDiscount from "../pages/AdditionalDiscount";
import TrialPaymentWithDiscount from "../pages/TrialPaymentWithDiscount";
ReactGA.initialize("G-00S3ECJGSJ");
const isProduction = import.meta.env.MODE === "production";
if (isProduction) {
ReactGA.initialize("G-00S3ECJGSJ");
}
function App(): JSX.Element {
const [isSpecialOfferOpen, setIsSpecialOfferOpen] = useState<boolean>(false);
@ -104,6 +110,7 @@ function App(): JSX.Element {
const { token, user } = useAuth();
useEffect(() => {
if (!isProduction) return;
ReactGA.send({
hitType: "pageview",
page: document.location.pathname + document.location.search,
@ -252,6 +259,14 @@ function App(): JSX.Element {
element={<EmailConfirmPage />}
/>
<Route path={routes.client.onboarding()} element={<OnboardingPage />} />
<Route
path={routes.client.additionalDiscount()}
element={<AdditionalDiscount />}
/>
<Route
path={routes.client.trialPaymentWithDiscount()}
element={<TrialPaymentWithDiscount />}
/>
<Route
path={routes.client.trialChoice()}
element={<TrialChoicePage />}

View File

@ -1,4 +1,4 @@
import { ReactNode } from "react";
import { ReactNode, useEffect } from "react";
import styles from "./styles.module.css";
interface ModalProps {
@ -16,8 +16,20 @@ function Modal({
}: ModalProps): JSX.Element {
const handleClose = (event: React.MouseEvent) => {
if (event.target !== event.currentTarget) return;
document.body.classList.remove("no-scroll");
onClose?.();
};
useEffect(() => {
if (open) {
document.body.classList.add("no-scroll");
}
return () => {
document.body.classList.remove("no-scroll");
};
}, [open]);
if (!open) return <></>;
return (
<div className={styles.modal} onClick={handleClose}>

View File

@ -1,6 +1,6 @@
.modal {
background: rgba(85,84,85,.8);
height: 100vh;
height: 100dvh;
position: fixed;
left: 0;
top: 0;

View File

@ -1,4 +1,5 @@
import { SubscriptionReceipts, useApi } from "@/api";
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";
@ -8,13 +9,19 @@ import CheckoutForm from "./CheckoutForm";
import { useAuth } from "@/auth";
import { useSelector } from "react-redux";
import { selectors } from "@/store";
import { PayPalReceiptPayload } from "@/api/resources/UserSubscriptionReceipts";
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 { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import routes from "@/routes";
interface StripeModalProps {
open: boolean;
onClose: () => void;
onSuccess: (receipt: SubscriptionReceipts.SubscriptionReceipt) => void;
onError: (error: Error) => void;
// onSuccess: (receipt: SubscriptionReceipts.SubscriptionReceipt) => void;
// onError: (error: Error) => void;
}
export function StripeModal({
@ -23,37 +30,55 @@ 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 [subPlans, setSubPlans] = useState<ISubscriptionPlan[] | null>(null);
const [clientSecret, setClientSecret] = useState<string>("");
const [subscriptionReceiptId, setSubscriptionReceiptId] =
useState<string>("");
const [isLoading, setIsLoading] = useState(true);
if (!activeSubPlan) {
navigate(routes.client.trialChoice());
}
useEffect(() => {
(async () => {
const siteConfig = await api.getAppConfig({ bundleId: "auraweb" });
setStripePromise(loadStripe(siteConfig.data.stripe_public_key));
const { sub_plans } = await api.getSubscriptionPlans({ locale });
setSubPlans(sub_plans);
const isActiveSubPlan = sub_plans.find(
(subPlan) => subPlan.id === activeSubPlan?.id
);
if (!activeSubPlan || !isActiveSubPlan) {
navigate(routes.client.priceList());
}
})();
}, [api]);
}, [activeSubPlan, api, locale, navigate]);
useEffect(() => {
if (!open) return;
(async () => {
const { subscription_receipt } = await api.createSubscriptionReceipt({
token,
way: "paypal",
itemInterval: "year",
way: "stripe",
subscription_receipt: {
sub_plan_id: activeSubPlan?.id || "",
sub_plan_id: activeSubPlan?.id || "stripe.7",
},
} as PayPalReceiptPayload);
});
const { id } = subscription_receipt;
const { client_secret } = subscription_receipt.data;
setSubscriptionReceiptId(id);
setClientSecret(client_secret);
setIsLoading(false);
})();
}, [api, token, open]);
}, [api, token]);
const handleClose = () => {
onClose();
@ -62,13 +87,29 @@ StripeModalProps): JSX.Element {
return (
<Modal open={open} onClose={handleClose}>
{isLoading ? (
<div className="payment-loader">
<div className={styles["payment-loader"]}>
<Loader />
</div>
) : null}
{stripePromise && clientSecret && (
{!isLoading && (
<>
<Title variant="h2" className={styles.title}>
Choose payment method
</Title>
<p className={styles.email}>{email}</p>
</>
)}
{stripePromise && clientSecret && subscriptionReceiptId && (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm />
<ApplePayButton
activeSubPlan={activeSubPlan}
client_secret={clientSecret}
subscriptionReceiptId={subscriptionReceiptId}
/>
{activeSubPlan && (
<SubPlanInformation subPlan={activeSubPlan} subPlans={subPlans} />
)}
<CheckoutForm subscriptionReceiptId={subscriptionReceiptId} />
</Elements>
)}
</Modal>

View File

@ -0,0 +1,38 @@
.page {
/* position: relative; */
position: static;
/* height: calc(100vh - 50px);
max-height: -webkit-fill-available; */
display: flex;
justify-items: center;
justify-content: center;
gap: 16px;
}
.payment-loader {
display: flex;
justify-content: center;
align-items: center;
}
.cross {
position: absolute;
top: -36px;
right: 28px;
width: 22px;
height: 22px;
cursor: pointer;
z-index: 9;
}
.title {
font-size: 27px;
font-weight: 700;
margin: 0;
}
.email {
font-size: 17px;
font-weight: 500;
margin: 0;
}

View File

@ -70,10 +70,10 @@ function ApplePayButton({
if (stripeError) {
// Show error to your customer (e.g., insufficient funds)
return;
return e.complete("fail");
}
navigate(`${routes.client.paymentResult()}/${subscriptionReceiptId}`);
navigate(`${routes.client.paymentResult()}/${subscriptionReceiptId}/`);
e.complete("success");
// Show a success message to your customer
// There's a risk of the customer closing the window before callback
// execution. Set up a webhook or plugin to listen for the

View File

@ -0,0 +1,40 @@
import Title from "@/components/Title";
import styles from "./styles.module.css";
import MainButton from "@/components/MainButton";
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
function AdditionalDiscount() {
const navigate = useNavigate();
const handleNext = () => {
navigate(routes.client.trialPaymentWithDiscount());
};
return (
<section className={`${styles.page} page`}>
<Title variant="h2" className={styles.title}>
Save 65% off!
</Title>
<img src="/friends.webp" alt="Friends" />
<div className={styles["discount-point"]}>
<img src="/fire.png" alt="Fire" />
<p className={styles["discount-point-description"]}>
65% off on your personalized plan
</p>
</div>
<div className={styles["discount-point"]}>
<img src="/present.png" alt="Present" />
<p className={styles["discount-point-description"]}>7-day trial</p>
</div>
<p className={styles["discount-description"]}>
<span>$9</span> instead of $19
</p>
<MainButton className={styles.button} onClick={handleNext}>
Get secret discount!
</MainButton>
</section>
);
}
export default AdditionalDiscount;

View File

@ -0,0 +1,69 @@
.page {
position: relative;
height: fit-content;
min-height: 100dvh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
background-position-y: top;
background-position-x: center;
background-size: contain;
background-repeat: no-repeat;
color: #fff;
padding-bottom: 64px;
background-image: url("/ellipse.webp");
background-color: #fff0f0;
color: #333333;
}
.title {
font-size: 32px;
line-height: 44px;
margin-bottom: 20px;
}
.discount-point {
display: flex;
flex-direction: row;
align-items: center;
justify-content: start;
gap: 20px;
width: 100%;
max-width: 262px;
margin-top: 20px;
}
.discount-point > img {
width: 48px;
}
.discount-point-description {
font-weight: 700;
font-size: 18px;
line-height: 140%;
color: #000;
}
.discount-description {
font-size: 16px;
margin-top: 20px;
}
.discount-description > span {
color: #8e8cf0;
font-weight: 700;
}
.button {
background: rgb(187, 107, 217);
height: 50px;
min-height: 0;
min-width: 0;
max-width: 300px;
box-shadow: rgba(0, 0, 0, 0.25) 0px 4px 4px 0px;
border-radius: 16px;
margin-top: 16px;
font-size: 16px;
font-weight: normal;
}

View File

@ -0,0 +1,30 @@
import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods";
import styles from "./styles.module.css";
interface IPaymentMethodsChoiceProps {
selectedPaymentMethod: EPaymentMethod;
onSelectPaymentMethod: (method: EPaymentMethod) => void;
}
function PaymentMethodsChoice({
selectedPaymentMethod,
onSelectPaymentMethod,
}: IPaymentMethodsChoiceProps) {
return (
<div className={styles["payment-methods"]}>
{paymentMethods.map((method, index) => (
<div
className={`${styles["payment-method"]} ${
selectedPaymentMethod === method.id && styles.active
}`}
onClick={() => onSelectPaymentMethod(method.id)}
key={index}
>
{method.component}
</div>
))}
</div>
);
}
export default PaymentMethodsChoice;

View File

@ -0,0 +1,23 @@
.payment-methods {
width: 100%;
display: flex;
justify-content: space-between;
}
.payment-method {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
border: 2px solid rgb(232, 232, 252);
border-radius: 12px;
max-width: 160px;
height: 68px;
padding-top: 8px;
padding-bottom: 8px;
}
.payment-method.active {
border-radius: 12px;
border: 2px solid rgb(114, 112, 192);
}

View File

@ -0,0 +1,23 @@
import MainButton from "@/components/MainButton";
import styles from "./styles.module.css";
import Loader from "@/components/Loader";
interface IPayPalButton {
isLoading: boolean;
handlePayPalButton: () => void;
}
function PayPalButton({ isLoading, handlePayPalButton }: IPayPalButton) {
return (
<MainButton
type="button"
className={styles["pay-pal-button"]}
onClick={handlePayPalButton}
>
{!isLoading && <img src="/paypal-logo.svg" alt="PayPal Button" />}
{isLoading && <Loader />}
</MainButton>
);
}
export default PayPalButton;

View File

@ -0,0 +1,9 @@
.pay-pal-button {
width: 100%;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background-color: #ffc43a;
border-radius: 7px;
}

View File

@ -0,0 +1,232 @@
import Title from "@/components/Title";
import styles from "./styles.module.css";
import PaymentMethodsChoice from "../PaymentMethodsChoice";
import { useEffect, useState } from "react";
import { EPaymentMethod } from "@/data/paymentMethods";
import { Elements } from "@stripe/react-stripe-js";
import ApplePayButton from "@/components/StripePage/ApplePayButton";
import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm";
import { Stripe, loadStripe } from "@stripe/stripe-js";
import { useDispatch, useSelector } from "react-redux";
import { actions, 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 PayPalButton from "./components/PayPalButton";
function PaymentModal() {
const { i18n } = useTranslation();
const locale = i18n.language;
const api = useApi();
const { token } = useAuth();
const navigate = useNavigate();
const dispatch = useDispatch();
const activeSubPlan = useSelector(selectors.selectActiveSubPlan);
const [payPalSubPlan, setPayPalSubPlan] = useState<ISubscriptionPlan>();
const subscriptionReceiptFromStore = useSelector(
selectors.selectSubscriptionReceipt
);
const [stripePromise, setStripePromise] =
useState<Promise<Stripe | null> | null>(null);
const [clientSecret, setClientSecret] = useState<string>("");
const [subscriptionReceiptId, setSubscriptionReceiptId] =
useState<string>("");
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[] | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isLoadingPayPal, setIsLoadingPayPal] = useState(false);
const [isError, setIsError] = useState(false);
const [errors, setErrors] = useState<string>("");
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
EPaymentMethod.CREDIT_CARD
);
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 });
setSubPlans(sub_plans);
const isActiveSubPlan = sub_plans.find(
(subPlan) => subPlan.id === activeSubPlan?.id
);
if (!activeSubPlan || !isActiveSubPlan) {
navigate(routes.client.priceList());
}
})();
}, [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",
},
});
if (!subscription_receipt && !subscriptionReceiptFromStore) {
setIsError(true);
setIsLoading(false);
} else if (!subscription_receipt && subscriptionReceiptFromStore) {
const { id } = subscriptionReceiptFromStore;
const { client_secret } = subscriptionReceiptFromStore.data;
setSubscriptionReceiptId(id);
setClientSecret(client_secret);
setIsLoading(false);
} else if (subscription_receipt) {
dispatch(
actions.payment.update({
subscriptionReceipt: subscription_receipt,
})
);
const { id } = subscription_receipt;
const { client_secret } = subscription_receipt.data;
setSubscriptionReceiptId(id);
setClientSecret(client_secret);
setIsLoading(false);
}
console.log(subscription_receipt);
} catch (error) {
if (subscriptionReceiptFromStore) {
console.log(1);
const { id } = subscriptionReceiptFromStore;
const { client_secret } = subscriptionReceiptFromStore.data;
setSubscriptionReceiptId(id);
setClientSecret(client_secret);
setIsLoading(false);
return;
} else {
setIsError(true);
setIsLoading(false);
}
console.log(error);
}
})();
}, [activeSubPlan?.id, api, dispatch, subscriptionReceiptFromStore, token]);
useEffect(() => {
if (!subPlans) return;
const paypalPlan = subPlans
.filter((plan: ISubscriptionPlan) => plan.provider === "paypal")
.filter((plan: ISubscriptionPlan) => {
if (activeSubPlan?.trial && plan?.trial) return true;
if (!activeSubPlan?.trial && !plan?.trial) return true;
return false;
})
.find((plan: ISubscriptionPlan) => {
if (activeSubPlan?.trial && plan?.trial) {
return plan?.trial?.price_cents === activeSubPlan?.trial?.price_cents;
}
if (!activeSubPlan?.trial && !plan?.trial) {
return plan?.name === activeSubPlan?.name;
}
return false;
});
setPayPalSubPlan(paypalPlan);
}, [activeSubPlan?.name, activeSubPlan?.trial, subPlans]);
const handlePayPalButton = async () => {
setIsLoadingPayPal(true);
const {
subscription_receipt: { data },
} = await api.createSubscriptionReceipt({
token,
way: "paypal",
subscription_receipt: {
sub_plan_id: payPalSubPlan?.id || "paypal.6",
},
});
if (!data?.links) {
return setErrors("Something went wrong. Please try again later.");
}
const link = data.links.find((link) => link.rel === "approve");
if (!link) {
return setErrors("Something went wrong. Please try again later.");
}
setIsLoadingPayPal(false);
window.location.href = link.href;
};
if (isLoading) {
return (
<div className={styles["payment-modal"]}>
<div className={styles["payment-loader"]}>
<Loader />
</div>
</div>
);
}
if (isError) {
return (
<div className={styles["payment-modal"]}>
<Title variant="h3" className={styles.title}>
Something went wrong
</Title>
</div>
);
}
return (
<div className={styles["payment-modal"]}>
<Title variant="h3" className={styles.title}>
Choose payment method
</Title>
<PaymentMethodsChoice
selectedPaymentMethod={selectedPaymentMethod}
onSelectPaymentMethod={onSelectPaymentMethod}
/>
{activeSubPlan && (
<p className={styles["sub-plan-description"]}>
You will be charged only{" "}
<b>
${getPriceFromTrial(activeSubPlan?.trial)} for your 3-day trial.
</b>
</p>
)}
<div className={styles["payment-method-container"]}>
{stripePromise && clientSecret && subscriptionReceiptId && (
<Elements stripe={stripePromise} options={{ clientSecret }}>
{selectedPaymentMethod === EPaymentMethod.PAYPAL_OR_APPLE_PAY && (
<>
<ApplePayButton
activeSubPlan={activeSubPlan}
client_secret={clientSecret}
subscriptionReceiptId={subscriptionReceiptId}
/>
{payPalSubPlan && (
<PayPalButton
isLoading={isLoadingPayPal}
handlePayPalButton={handlePayPalButton}
/>
)}
{!!errors.length && <p className={styles.errors}>{errors}</p>}
</>
)}
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
<CheckoutForm subscriptionReceiptId={subscriptionReceiptId} />
)}
</Elements>
)}
</div>
<SecurityPayments />
</div>
);
}
export default PaymentModal;

View File

@ -0,0 +1,25 @@
.payment-modal {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 250px;
gap: 25px;
}
.title {
font-weight: 800;
font-size: 20px;
line-height: 20px;
text-align: center;
margin: 0;
}
.sub-plan-description {
font-size: 12px;
text-align: center;
}
.payment-method-container {
width: 100%;
}

View File

@ -0,0 +1,10 @@
import styles from "./styles.module.css";
function SecurityPayments() {
return <div className={styles.container}>
<img src="/security.svg" alt="Guaranteed security" />
<p className={styles.text}>Guaranteed security payments</p>
</div>;
}
export default SecurityPayments;

View File

@ -0,0 +1,12 @@
.container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 6px;
}
.text {
font-size: 12px;
font-weight: 400;
}

View File

@ -4,8 +4,8 @@ import PersonalInformation from "./components/PersonalInformation";
import styles from "./styles.module.css";
import Goal from "./components/Goal";
import PaymentTable from "./components/PaymentTable";
import { useSelector } from "react-redux";
import { selectors } from "@/store";
import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store";
import { Navigate, useNavigate, useParams } from "react-router-dom";
import routes from "@/routes";
import { getZodiacSignByDate } from "@/services/zodiac-sign";
@ -19,10 +19,13 @@ 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";
const locale = getClientLocale() as Locale;
function TrialPaymentPage() {
const dispatch = useDispatch();
const api = useApi();
const navigate = useNavigate();
const birthdate = useSelector(selectors.selectBirthdate);
@ -42,6 +45,7 @@ function TrialPaymentPage() {
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"
@ -81,9 +85,10 @@ function TrialPaymentPage() {
);
if (targetSubPlan) {
setActiveSubPlan(targetSubPlan);
dispatch(actions.payment.update({ activeSubPlan: targetSubPlan }));
}
}
}, [subPlan, subPlans]);
}, [dispatch, subPlan, subPlans]);
useEffect(() => {
if (["relationship", "married"].includes(flowChoice)) {
@ -103,13 +108,21 @@ function TrialPaymentPage() {
return <Navigate to={routes.client.gender()} />;
}
const handleNext = () => {
navigate(routes.client.paymentStripe());
const handleDiscount = () => {
setIsOpenPaymentModal(false);
navigate(routes.client.additionalDiscount());
};
const openStripeModal = () => {
setIsOpenPaymentModal(true);
};
return (
<section className={`${styles.page} page`}>
<Header buttonClick={handleNext} />
<Modal open={isOpenPaymentModal} onClose={handleDiscount}>
<PaymentModal />
</Modal>
<Header buttonClick={openStripeModal} />
{singleOrWithPartner === "partner" && (
<WithPartnerInformation
zodiacSign={zodiacSign}
@ -138,17 +151,17 @@ function TrialPaymentPage() {
Your Personalized Clarity & Love Reading is ready!
</Title>
<Goal goal={goal} />
<PaymentTable subPlan={activeSubPlan} buttonClick={handleNext} />
<PaymentTable subPlan={activeSubPlan} buttonClick={openStripeModal} />
<YourReading
gender={gender}
zodiacSign={zodiacSign}
buttonClick={handleNext}
buttonClick={openStripeModal}
singleOrWithPartner={singleOrWithPartner}
/>
<Reviews />
<YouGet />
<OftenAsk />
<PaymentTable subPlan={activeSubPlan} buttonClick={handleNext} />
<PaymentTable subPlan={activeSubPlan} buttonClick={openStripeModal} />
</section>
);
}

View File

@ -0,0 +1,43 @@
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";
function PaymentDiscountTable() {
const activeSub = useSelector(selectors.selectActiveSubPlan);
return (
<div className={styles.container}>
<Title variant="h3" className={styles.title}>
You get a secret discount!
</Title>
<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" />
<p className={styles.description}>Secret discount applied!</p>
</div>
<div className={styles.side}>
<span className={styles.discount}>-30%</span>
<strong>-65%</strong>
</div>
</div>
<div className={styles["cost-container"]}>
<p>Your cost per 1 month after trial:</p>
<div className={styles.side}>
<span className={styles.discount}>$19</span>
<strong>$9</strong>
</div>
</div>
<p className={styles.save}>You save $30</p>
<hr className={styles.line} />
<div className={styles["total-container"]}>
<p>Total today:</p>
{activeSub && <strong>${getPriceFromTrial(activeSub.trial)}</strong>}
</div>
</div>
);
}
export default PaymentDiscountTable;

View File

@ -0,0 +1,156 @@
.container {
padding: 16px 12px;
border-radius: 8px;
margin-top: 20px;
width: 100%;
background-color: #fbfbff;
}
.title {
font-size: 18px;
line-height: 28px;
text-align: center;
color: #0f0f0f;
}
.no-pressure {
font-weight: 600;
font-size: 14px;
line-height: 25px;
text-align: center;
margin-top: 4px;
color: #4f4f4f;
}
.applied {
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(
95.17deg,
rgb(207, 139, 243) -16.49%,
rgb(167, 112, 239) -15.14%,
rgb(253, 185, 155) 115.23%
);
border-radius: 6px;
margin-top: 16px;
padding: 3px 9px;
}
.present-image {
width: 20px;
}
.applied .description {
font-size: 14px;
font-weight: 700;
line-height: 24px;
color: rgb(251, 251, 255);
}
.applied > .side {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.discount {
position: relative;
font-weight: 400;
font-size: 14px;
color: rgb(51, 51, 51);
line-height: 19px;
margin-right: 10px;
}
.discount::before {
position: absolute;
content: "";
left: -2px;
top: 50%;
right: -2px;
border-top: 1px solid rgb(235, 87, 87);
border-right-color: rgb(235, 87, 87);
border-bottom-color: rgb(235, 87, 87);
border-left-color: rgb(235, 87, 87);
transform: rotate(8deg);
}
.applied strong {
font-weight: 800;
font-size: 14px;
line-height: 19px;
color: rgb(15, 15, 15);
}
.cost-container {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-top: 8px;
}
.cost-container > p {
font-size: 14px;
line-height: 24px;
font-weight: 600;
color: rgb(79, 79, 79);
}
.cost-container .discount {
color: rgb(130, 130, 130);
}
.cost-container strong {
font-size: 14px;
font-weight: 600;
line-height: 19px;
color: rgb(51, 51, 51);
}
.save {
font-size: 14px;
font-weight: 600;
line-height: 24px;
color: rgb(32, 31, 31);
}
.line {
height: 1px;
background-color: rgb(153, 116, 246);
margin-top: 16px;
margin-bottom: 16px;
box-sizing: content-box;
}
.total-container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.total-container > p {
font-size: 16px;
font-weight: 600;
line-height: 24px;
color: rgb(15, 15, 15);
}
.total-container strong {
font-size: 18px;
font-weight: 700;
line-height: 24px;
background: linear-gradient(
165.54deg,
rgb(20, 19, 51) -33.39%,
rgb(32, 34, 97) 15.89%,
rgb(84, 60, 151) 55.84%,
rgb(105, 57, 162) 74.96%
)
text;
-webkit-text-fill-color: transparent;
}

View File

@ -0,0 +1,47 @@
import Title from "@/components/Title";
import styles from "./styles.module.css";
import MainButton from "@/components/MainButton";
import PaymentDiscountTable from "./PaymentDiscountTable";
import Modal from "@/components/Modal";
import PaymentModal from "../TrialPayment/components/PaymentModal";
import { useState } from "react";
function TrialPaymentWithDiscount() {
const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false);
const handleClose = () => {
setIsOpenPaymentModal(false);
};
return (
<section className={`${styles.page} page`}>
<Modal open={isOpenPaymentModal} onClose={handleClose}>
<PaymentModal />
</Modal>
<img
className={styles["party-popper"]}
src="/party_popper.png"
alt="Party popper"
/>
<Title variant="h2" className={styles.title}>
You get a secret discount!
</Title>
<PaymentDiscountTable />
<MainButton
className={styles.button}
onClick={() => setIsOpenPaymentModal(true)}
>
Start your 3-day trial
</MainButton>
<p className={styles.policy}>
By continuing you agree that if you don't cancel prior to the end of the
3-days trial, you will automatically be charged $9 for the introductory
period of 30 days thereafter the standard rate of $9 every 30 days until
you cancel in settings. Learn more about cancellation and refund policy
in Subscription terms.
</p>
</section>
);
}
export default TrialPaymentWithDiscount;

View File

@ -0,0 +1,58 @@
.page {
position: relative;
height: fit-content;
min-height: 100dvh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
color: #fff;
padding-bottom: 64px;
background-color: #fff0f0;
}
.party-popper {
width: 32px;
}
.title {
margin-top: 11px;
font-size: 24px;
line-height: 33px;
text-align: center;
background: linear-gradient(
165.54deg,
rgb(20, 19, 51) -33.39%,
rgb(32, 34, 97) 15.89%,
rgb(84, 60, 151) 55.84%,
rgb(105, 57, 162) 74.96%
)
text;
-webkit-text-fill-color: transparent;
font-weight: 700;
}
.button {
margin-top: 32px;
border-radius: 30px;
min-height: 0;
height: 50px;
font-size: 20px;
font-weight: normal;
line-height: 22px;
width: 100%;
min-width: 0;
max-width: 360px;
background: #27ae60;
color: #fbfbff;
}
.policy {
color: rgb(51, 51, 51);
font-size: 13px;
font-weight: 400;
line-height: 20px;
margin-top: 28px;
padding-bottom: 10px;
max-width: 400px;
}

View File

@ -0,0 +1,12 @@
import styles from "./styles.module.css"
function CreditCard() {
return (
<div className={styles.container}>
<img src="/credit-card.svg" alt="Credit card" />
<span className={styles.text}>Credit Card</span>
</div>
)
}
export default CreditCard

View File

@ -0,0 +1,14 @@
.container {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 10px;
}
.text {
font-weight: 600;
line-height: 21px;
color: rgb(99, 99, 157);
}

View File

@ -0,0 +1,10 @@
import styles from "./styles.module.css";
function PayPalOrApplePay() {
return <div className={styles.container}>
<img src="/paypal.webp" alt="PayPal" />
<img src="/applepay.webp" alt="ApplePay" />
</div>;
}
export default PayPalOrApplePay;

View File

@ -0,0 +1,12 @@
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
gap: 8px;
}
.container > img {
height: 16px;
}

View File

@ -0,0 +1,23 @@
import CreditCard from "@/components/ui/PaymentMethodsButtons/CreditCard";
import PayPalOrApplePay from "@/components/ui/PaymentMethodsButtons/PayPayOrApplePay";
export enum EPaymentMethod {
CREDIT_CARD = "card",
PAYPAL_OR_APPLE_PAY = "payPalOrApplePay",
}
interface IPaymentMethod {
id: EPaymentMethod;
component: JSX.Element;
}
export const paymentMethods: IPaymentMethod[] = [
{
id: EPaymentMethod.PAYPAL_OR_APPLE_PAY,
component: <PayPalOrApplePay />,
},
{
id: EPaymentMethod.CREDIT_CARD,
component: <CreditCard />,
},
];

View File

@ -22,7 +22,8 @@ h4 {
font-weight: 400;
}
button,h4 {
button,
h4 {
font-size: 18px;
}
@ -36,18 +37,109 @@ input {
outline: none;
}
a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video {
a,
abbr,
acronym,
address,
applet,
article,
aside,
audio,
b,
big,
blockquote,
body,
canvas,
caption,
center,
cite,
code,
dd,
del,
details,
dfn,
div,
dl,
dt,
em,
embed,
fieldset,
figcaption,
figure,
footer,
form,
h1,
h2,
h3,
h4,
h5,
h6,
header,
hgroup,
html,
i,
iframe,
img,
ins,
kbd,
label,
legend,
li,
mark,
menu,
nav,
object,
ol,
output,
p,
pre,
q,
ruby,
s,
samp,
section,
small,
span,
strike,
strong,
sub,
summary,
sup,
table,
tbody,
td,
tfoot,
th,
thead,
time,
tr,
tt,
u,
ul,
var,
video {
border: 0;
margin: 0;
padding: 0;
vertical-align: initial;
}
article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section {
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body,html {
body,
html {
height: 100%;
line-height: 1;
}
@ -56,7 +148,8 @@ body,html {
height: auto;
}
ol,ul {
ol,
ul {
list-style: none;
}
@ -65,8 +158,8 @@ a {
text-decoration: none;
}
div[class^=divider] {
font-family: SF Pro Text Regular,sans-serif
div[class^="divider"] {
font-family: SF Pro Text Regular, sans-serif;
}
* {
@ -93,7 +186,12 @@ div[class^=divider] {
height: 100%;
}
a,button,div,input,select,textarea {
a,
button,
div,
input,
select,
textarea {
-webkit-tap-highlight-color: transparent;
}
@ -134,4 +232,9 @@ a,button,div,input,select,textarea {
left: 0;
top: 0;
width: 100%;
}
}
.no-scroll {
position: fixed;
overflow: hidden;
}

View File

@ -37,13 +37,14 @@ const init = async () => {
.init(options);
window.Chargebee.init(config.chargebee);
const isProduction = import.meta.env.MODE === "production";
// SCRIPTS TO HEAD
const yandexMetric = () => {
const script = document.createElement("script");
script.setAttribute("src", "/metrics/yandex.js");
document.head.appendChild(script);
};
yandexMetric();
const smartLook = () => {
if (!config.smartlook_manage) return;
@ -51,7 +52,10 @@ const init = async () => {
script.setAttribute("src", "/metrics/smartlook.js");
document.head.appendChild(script);
};
smartLook();
if (isProduction) {
yandexMetric();
smartLook();
}
// const googleManager = () => {
// const script = document.createElement("script");
@ -59,7 +63,7 @@ const init = async () => {
// document.head.appendChild(script);
// };
// googleManager();
return (
<React.Fragment>
<I18nextProvider i18n={i18nextInstance}>

View File

@ -74,6 +74,9 @@ const routes = {
onboarding: () => [host, "onboarding"].join("/"),
trialChoice: () => [host, "trial-choice"].join("/"),
trialPayment: () => [host, "trial-payment"].join("/"),
additionalDiscount: () => [host, "additional-discount"].join("/"),
trialPaymentWithDiscount: () =>
[host, "trial-payment-with-discount"].join("/"),
notFound: () => [host, "404"].join("/"),
},
server: {
@ -221,6 +224,8 @@ export const withoutFooterRoutes = [
routes.client.onboarding(),
routes.client.trialChoice(),
routes.client.trialPayment(),
routes.client.additionalDiscount(),
routes.client.trialPaymentWithDiscount(),
];
export const withoutFooterPartOfRoutes = [routes.client.questionnaire()];
@ -281,6 +286,8 @@ export const withoutHeaderRoutes = [
routes.client.satisfiedResult(),
routes.client.onboarding(),
routes.client.trialPayment(),
routes.client.additionalDiscount(),
routes.client.trialPaymentWithDiscount(),
];
export const hasNoHeader = (path: string) => {
return !withoutHeaderRoutes.includes(`/${path.split("/")[1]}`);

View File

@ -4,7 +4,10 @@ import {
createAction,
} from "@reduxjs/toolkit";
import token, { actions as tokenActions, selectToken } from "./token";
import questionnaire, { actions as questionnaireActions, selectQuestionnaire } from "./questionnaire";
import questionnaire, {
actions as questionnaireActions,
selectQuestionnaire,
} from "./questionnaire";
import user, { actions as userActions, selectUser } from "./user";
import form, {
actions as formActions,
@ -27,6 +30,7 @@ import payment, {
actions as paymentActions,
selectActiveSubPlan,
selectIsDiscount,
selectSubscriptionReceipt,
} from "./payment";
import subscriptionPlans, {
actions as subscriptionPlasActions,
@ -85,6 +89,7 @@ export const selectors = {
selectUserCallbacksPrevStat,
selectHome,
selectIsDiscount,
selectSubscriptionReceipt,
selectOnboarding,
selectOnboardingHome,
selectOnboardingCompatibility,
@ -108,7 +113,7 @@ export const reducer = combineReducers({
userCallbacks,
siteConfig,
onboardingConfig,
questionnaire
questionnaire,
});
export type RootState = ReturnType<typeof reducer>;

View File

@ -1,4 +1,5 @@
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import { SubscriptionReceipt } from "@/api/resources/UserSubscriptionReceipts";
import { createSlice, createSelector } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";
@ -6,12 +7,14 @@ interface IPayment {
selectedPrice: number | null;
isDiscount: boolean;
activeSubPlan: ISubscriptionPlan | null;
subscriptionReceipt: SubscriptionReceipt | null;
}
const initialState: IPayment = {
selectedPrice: null,
isDiscount: false,
activeSubPlan: null
activeSubPlan: null,
subscriptionReceipt: null,
};
const paymentSlice = createSlice({
@ -38,4 +41,8 @@ export const selectIsDiscount = createSelector(
(state: { payment: IPayment }) => state.payment.isDiscount,
(payment) => payment
);
export const selectSubscriptionReceipt = createSelector(
(state: { payment: IPayment }) => state.payment.subscriptionReceipt,
(payment) => payment
);
export default paymentSlice.reducer;