diff --git a/public/applepay.webp b/public/applepay.webp new file mode 100644 index 0000000..3669207 Binary files /dev/null and b/public/applepay.webp differ diff --git a/public/credit-card.svg b/public/credit-card.svg new file mode 100644 index 0000000..ad1d1a0 --- /dev/null +++ b/public/credit-card.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/public/ellipse.webp b/public/ellipse.webp new file mode 100644 index 0000000..215b554 Binary files /dev/null and b/public/ellipse.webp differ diff --git a/public/fire.png b/public/fire.png new file mode 100644 index 0000000..a4525ad Binary files /dev/null and b/public/fire.png differ diff --git a/public/friends.webp b/public/friends.webp new file mode 100644 index 0000000..aee1fba Binary files /dev/null and b/public/friends.webp differ diff --git a/public/paypal.webp b/public/paypal.webp new file mode 100644 index 0000000..d391eda Binary files /dev/null and b/public/paypal.webp differ diff --git a/public/present.png b/public/present.png new file mode 100644 index 0000000..73bc6e5 Binary files /dev/null and b/public/present.png differ diff --git a/public/security.svg b/public/security.svg new file mode 100644 index 0000000..446a9c7 --- /dev/null +++ b/public/security.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 7c6d7c9..331b8dc 100755 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -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(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={} /> } /> + } + /> + } + /> } diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index ded1489..1eff20c 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -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 (
diff --git a/src/components/Modal/styles.module.css b/src/components/Modal/styles.module.css index ffdbec5..245e4c5 100644 --- a/src/components/Modal/styles.module.css +++ b/src/components/Modal/styles.module.css @@ -1,6 +1,6 @@ .modal { background: rgba(85,84,85,.8); - height: 100vh; + height: 100dvh; position: fixed; left: 0; top: 0; diff --git a/src/components/PaymentPage/methods/Stripe/Modal.tsx b/src/components/PaymentPage/methods/Stripe/Modal.tsx index f38259e..b37d61c 100644 --- a/src/components/PaymentPage/methods/Stripe/Modal.tsx +++ b/src/components/PaymentPage/methods/Stripe/Modal.tsx @@ -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 | null>(null); + const [subPlans, setSubPlans] = useState(null); const [clientSecret, setClientSecret] = useState(""); + const [subscriptionReceiptId, setSubscriptionReceiptId] = + useState(""); 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 ( {isLoading ? ( -
+
) : null} - {stripePromise && clientSecret && ( + {!isLoading && ( + <> + + Choose payment method + +

{email}

+ + )} + {stripePromise && clientSecret && subscriptionReceiptId && ( - + + {activeSubPlan && ( + + )} + )} diff --git a/src/components/PaymentPage/methods/Stripe/styles.module.css b/src/components/PaymentPage/methods/Stripe/styles.module.css new file mode 100644 index 0000000..1a43dab --- /dev/null +++ b/src/components/PaymentPage/methods/Stripe/styles.module.css @@ -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; +} diff --git a/src/components/StripePage/ApplePayButton/index.tsx b/src/components/StripePage/ApplePayButton/index.tsx index 0039355..ba3e47d 100644 --- a/src/components/StripePage/ApplePayButton/index.tsx +++ b/src/components/StripePage/ApplePayButton/index.tsx @@ -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 diff --git a/src/components/pages/AdditionalDiscount/index.tsx b/src/components/pages/AdditionalDiscount/index.tsx new file mode 100644 index 0000000..e3b5c3b --- /dev/null +++ b/src/components/pages/AdditionalDiscount/index.tsx @@ -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 ( +
+ + Save 65% off! + + Friends +
+ Fire +

+ 65% off on your personalized plan +

+
+
+ Present +

7-day trial

+
+

+ $9 instead of $19 +

+ + Get secret discount! + +
+ ); +} + +export default AdditionalDiscount; diff --git a/src/components/pages/AdditionalDiscount/styles.module.css b/src/components/pages/AdditionalDiscount/styles.module.css new file mode 100644 index 0000000..3d8e28a --- /dev/null +++ b/src/components/pages/AdditionalDiscount/styles.module.css @@ -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; +} diff --git a/src/components/pages/TrialPayment/components/PaymentMethodsChoice/index.tsx b/src/components/pages/TrialPayment/components/PaymentMethodsChoice/index.tsx new file mode 100644 index 0000000..950eea6 --- /dev/null +++ b/src/components/pages/TrialPayment/components/PaymentMethodsChoice/index.tsx @@ -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 ( +
+ {paymentMethods.map((method, index) => ( +
onSelectPaymentMethod(method.id)} + key={index} + > + {method.component} +
+ ))} +
+ ); +} + +export default PaymentMethodsChoice; diff --git a/src/components/pages/TrialPayment/components/PaymentMethodsChoice/styles.module.css b/src/components/pages/TrialPayment/components/PaymentMethodsChoice/styles.module.css new file mode 100644 index 0000000..fb9e06c --- /dev/null +++ b/src/components/pages/TrialPayment/components/PaymentMethodsChoice/styles.module.css @@ -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); +} diff --git a/src/components/pages/TrialPayment/components/PaymentModal/components/PayPalButton/index.tsx b/src/components/pages/TrialPayment/components/PaymentModal/components/PayPalButton/index.tsx new file mode 100644 index 0000000..5fb253f --- /dev/null +++ b/src/components/pages/TrialPayment/components/PaymentModal/components/PayPalButton/index.tsx @@ -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 ( + + {!isLoading && PayPal Button} + {isLoading && } + + ); +} + +export default PayPalButton; diff --git a/src/components/pages/TrialPayment/components/PaymentModal/components/PayPalButton/styles.module.css b/src/components/pages/TrialPayment/components/PaymentModal/components/PayPalButton/styles.module.css new file mode 100644 index 0000000..b75606d --- /dev/null +++ b/src/components/pages/TrialPayment/components/PaymentModal/components/PayPalButton/styles.module.css @@ -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; +} diff --git a/src/components/pages/TrialPayment/components/PaymentModal/index.tsx b/src/components/pages/TrialPayment/components/PaymentModal/index.tsx new file mode 100644 index 0000000..472a63b --- /dev/null +++ b/src/components/pages/TrialPayment/components/PaymentModal/index.tsx @@ -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(); + const subscriptionReceiptFromStore = useSelector( + selectors.selectSubscriptionReceipt + ); + const [stripePromise, setStripePromise] = + useState | null>(null); + const [clientSecret, setClientSecret] = useState(""); + const [subscriptionReceiptId, setSubscriptionReceiptId] = + useState(""); + const [subPlans, setSubPlans] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isLoadingPayPal, setIsLoadingPayPal] = useState(false); + const [isError, setIsError] = useState(false); + const [errors, setErrors] = useState(""); + + 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 ( +
+
+ +
+
+ ); + } + + if (isError) { + return ( +
+ + Something went wrong + +
+ ); + } + + return ( +
+ + Choose payment method + + + {activeSubPlan && ( +

+ You will be charged only{" "} + + ${getPriceFromTrial(activeSubPlan?.trial)} for your 3-day trial. + +

+ )} +
+ {stripePromise && clientSecret && subscriptionReceiptId && ( + + {selectedPaymentMethod === EPaymentMethod.PAYPAL_OR_APPLE_PAY && ( + <> + + {payPalSubPlan && ( + + )} + {!!errors.length &&

{errors}

} + + )} + + {selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && ( + + )} +
+ )} +
+ +
+ ); +} + +export default PaymentModal; diff --git a/src/components/pages/TrialPayment/components/PaymentModal/styles.module.css b/src/components/pages/TrialPayment/components/PaymentModal/styles.module.css new file mode 100644 index 0000000..900e309 --- /dev/null +++ b/src/components/pages/TrialPayment/components/PaymentModal/styles.module.css @@ -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%; +} diff --git a/src/components/pages/TrialPayment/components/SecurityPayments/index.tsx b/src/components/pages/TrialPayment/components/SecurityPayments/index.tsx new file mode 100644 index 0000000..160a317 --- /dev/null +++ b/src/components/pages/TrialPayment/components/SecurityPayments/index.tsx @@ -0,0 +1,10 @@ +import styles from "./styles.module.css"; + +function SecurityPayments() { + return
+ Guaranteed security +

Guaranteed security payments

+
; +} + +export default SecurityPayments; diff --git a/src/components/pages/TrialPayment/components/SecurityPayments/styles.module.css b/src/components/pages/TrialPayment/components/SecurityPayments/styles.module.css new file mode 100644 index 0000000..77bdb21 --- /dev/null +++ b/src/components/pages/TrialPayment/components/SecurityPayments/styles.module.css @@ -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; +} diff --git a/src/components/pages/TrialPayment/index.tsx b/src/components/pages/TrialPayment/index.tsx index 4ff0337..8a841bf 100755 --- a/src/components/pages/TrialPayment/index.tsx +++ b/src/components/pages/TrialPayment/index.tsx @@ -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( activeSubPlanFromStore ); + const [isOpenPaymentModal, setIsOpenPaymentModal] = useState(false); const [marginTopTitle, setMarginTopTitle] = useState(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 ; } - const handleNext = () => { - navigate(routes.client.paymentStripe()); + const handleDiscount = () => { + setIsOpenPaymentModal(false); + navigate(routes.client.additionalDiscount()); + }; + + const openStripeModal = () => { + setIsOpenPaymentModal(true); }; return (
-
+ + + +
{singleOrWithPartner === "partner" && ( - + - +
); } diff --git a/src/components/pages/TrialPaymentWithDiscount/PaymentDiscountTable/index.tsx b/src/components/pages/TrialPaymentWithDiscount/PaymentDiscountTable/index.tsx new file mode 100644 index 0000000..b62b2f7 --- /dev/null +++ b/src/components/pages/TrialPaymentWithDiscount/PaymentDiscountTable/index.tsx @@ -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 ( +
+ + You get a secret discount! + +

No pressure. Cancel anytime.

+
+
+ Present +

Secret discount applied!

+
+
+ -30% + -65% +
+
+
+

Your cost per 1 month after trial:

+
+ $19 + $9 +
+
+

You save $30

+
+
+

Total today:

+ {activeSub && ${getPriceFromTrial(activeSub.trial)}} +
+
+ ); +} + +export default PaymentDiscountTable; diff --git a/src/components/pages/TrialPaymentWithDiscount/PaymentDiscountTable/styles.module.css b/src/components/pages/TrialPaymentWithDiscount/PaymentDiscountTable/styles.module.css new file mode 100644 index 0000000..b41ff23 --- /dev/null +++ b/src/components/pages/TrialPaymentWithDiscount/PaymentDiscountTable/styles.module.css @@ -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; +} diff --git a/src/components/pages/TrialPaymentWithDiscount/index.tsx b/src/components/pages/TrialPaymentWithDiscount/index.tsx new file mode 100644 index 0000000..972cdd9 --- /dev/null +++ b/src/components/pages/TrialPaymentWithDiscount/index.tsx @@ -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(false); + + const handleClose = () => { + setIsOpenPaymentModal(false); + }; + + return ( +
+ + + + Party popper + + You get a secret discount! + + + setIsOpenPaymentModal(true)} + > + Start your 3-day trial + +

+ 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. +

+
+ ); +} + +export default TrialPaymentWithDiscount; diff --git a/src/components/pages/TrialPaymentWithDiscount/styles.module.css b/src/components/pages/TrialPaymentWithDiscount/styles.module.css new file mode 100644 index 0000000..4049a7a --- /dev/null +++ b/src/components/pages/TrialPaymentWithDiscount/styles.module.css @@ -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; +} diff --git a/src/components/ui/PaymentMethodsButtons/CreditCard/index.tsx b/src/components/ui/PaymentMethodsButtons/CreditCard/index.tsx new file mode 100644 index 0000000..83124c8 --- /dev/null +++ b/src/components/ui/PaymentMethodsButtons/CreditCard/index.tsx @@ -0,0 +1,12 @@ +import styles from "./styles.module.css" + +function CreditCard() { + return ( +
+ Credit card + Credit Card +
+ ) +} + +export default CreditCard \ No newline at end of file diff --git a/src/components/ui/PaymentMethodsButtons/CreditCard/styles.module.css b/src/components/ui/PaymentMethodsButtons/CreditCard/styles.module.css new file mode 100644 index 0000000..f272c57 --- /dev/null +++ b/src/components/ui/PaymentMethodsButtons/CreditCard/styles.module.css @@ -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); +} diff --git a/src/components/ui/PaymentMethodsButtons/PayPayOrApplePay/index.tsx b/src/components/ui/PaymentMethodsButtons/PayPayOrApplePay/index.tsx new file mode 100644 index 0000000..9b9b4b1 --- /dev/null +++ b/src/components/ui/PaymentMethodsButtons/PayPayOrApplePay/index.tsx @@ -0,0 +1,10 @@ +import styles from "./styles.module.css"; + +function PayPalOrApplePay() { + return
+ PayPal + ApplePay +
; +} + +export default PayPalOrApplePay; diff --git a/src/components/ui/PaymentMethodsButtons/PayPayOrApplePay/styles.module.css b/src/components/ui/PaymentMethodsButtons/PayPayOrApplePay/styles.module.css new file mode 100644 index 0000000..7d1360f --- /dev/null +++ b/src/components/ui/PaymentMethodsButtons/PayPayOrApplePay/styles.module.css @@ -0,0 +1,12 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + gap: 8px; +} + +.container > img { + height: 16px; +} \ No newline at end of file diff --git a/src/data/paymentMethods.tsx b/src/data/paymentMethods.tsx new file mode 100644 index 0000000..bafe93a --- /dev/null +++ b/src/data/paymentMethods.tsx @@ -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: , + }, + { + id: EPaymentMethod.CREDIT_CARD, + component: , + }, +]; diff --git a/src/index.css b/src/index.css index cec9a22..963cec1 100644 --- a/src/index.css +++ b/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 newline at end of file +} + +.no-scroll { + position: fixed; + overflow: hidden; +} diff --git a/src/init.tsx b/src/init.tsx index ab725b2..c8573fb 100755 --- a/src/init.tsx +++ b/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 ( diff --git a/src/routes.ts b/src/routes.ts index b443700..7b67cfb 100755 --- a/src/routes.ts +++ b/src/routes.ts @@ -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]}`); diff --git a/src/store/index.ts b/src/store/index.ts index e06ad61..68cdcd6 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -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; diff --git a/src/store/payment.ts b/src/store/payment.ts index 908ad06..2f7631e 100644 --- a/src/store/payment.ts +++ b/src/store/payment.ts @@ -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;