Preview/discount pages
BIN
public/applepay.webp
Normal file
|
After Width: | Height: | Size: 662 B |
15
public/credit-card.svg
Normal 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
|
After Width: | Height: | Size: 6.2 KiB |
BIN
public/fire.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/friends.webp
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
public/paypal.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
public/present.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
6
public/security.svg
Normal 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 |
@ -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 />}
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
.modal {
|
||||
background: rgba(85,84,85,.8);
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
@ -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>
|
||||
|
||||
38
src/components/PaymentPage/methods/Stripe/styles.module.css
Normal 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;
|
||||
}
|
||||
@ -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
|
||||
|
||||
40
src/components/pages/AdditionalDiscount/index.tsx
Normal 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;
|
||||
69
src/components/pages/AdditionalDiscount/styles.module.css
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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%;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
47
src/components/pages/TrialPaymentWithDiscount/index.tsx
Normal 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;
|
||||
@ -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;
|
||||
}
|
||||
12
src/components/ui/PaymentMethodsButtons/CreditCard/index.tsx
Normal 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
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
@ -0,0 +1,12 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.container > img {
|
||||
height: 16px;
|
||||
}
|
||||
23
src/data/paymentMethods.tsx
Normal 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 />,
|
||||
},
|
||||
];
|
||||
121
src/index.css
@ -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;
|
||||
}
|
||||
|
||||
10
src/init.tsx
@ -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}>
|
||||
|
||||
@ -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]}`);
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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;
|
||||
|
||||