diff --git a/package-lock.json b/package-lock.json index 719dc8d..ef5a2a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "dependencies": { "@chargebee/chargebee-js-react-wrapper": "^0.6.3", "@reduxjs/toolkit": "^1.9.5", + "@stripe/react-stripe-js": "^2.3.1", + "@stripe/stripe-js": "^2.1.9", "apng-js": "^1.1.1", "html-react-parser": "^3.0.16", "i18next": "^22.5.0", @@ -1000,6 +1002,24 @@ "node": ">=14" } }, + "node_modules/@stripe/react-stripe-js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.3.1.tgz", + "integrity": "sha512-vXiwcG2ZjAF4AezjP7DJ8jiwxfCWCen/X2rBhyXaKrfQ7+pwmXhsoUlKRa0eLWioY1oelOQOafauNUiwTwFHgQ==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": "^1.44.1 || ^2.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-2.1.9.tgz", + "integrity": "sha512-0RSvCJrzEVx52e8hbSAcZ2vv6OzoFj5fe5XC50GSrcev1Y4t2XDE6W5CIhR/Y6l3CPgO/P4luqoLWuvpUkBhig==" + }, "node_modules/@types/history": { "version": "4.7.11", "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", @@ -2606,6 +2626,14 @@ "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", "dev": true }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2765,6 +2793,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -4012,6 +4055,19 @@ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.6.2.tgz", "integrity": "sha512-LzqpSrMK/3JBAVBI9u3NWtOhWNw5AMQfrUFYB0+bDHTSw17z++WJLsPsxAuK+oSddsxk4d7F/JcdDPM1M5YAhA==" }, + "@stripe/react-stripe-js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.3.1.tgz", + "integrity": "sha512-vXiwcG2ZjAF4AezjP7DJ8jiwxfCWCen/X2rBhyXaKrfQ7+pwmXhsoUlKRa0eLWioY1oelOQOafauNUiwTwFHgQ==", + "requires": { + "prop-types": "^15.7.2" + } + }, + "@stripe/stripe-js": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-2.1.9.tgz", + "integrity": "sha512-0RSvCJrzEVx52e8hbSAcZ2vv6OzoFj5fe5XC50GSrcev1Y4t2XDE6W5CIhR/Y6l3CPgO/P4luqoLWuvpUkBhig==" + }, "@types/history": { "version": "4.7.11", "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", @@ -5172,6 +5228,11 @@ "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", "dev": true }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5275,6 +5336,23 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, "punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", diff --git a/package.json b/package.json index b13ef08..a106ac7 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "dependencies": { "@chargebee/chargebee-js-react-wrapper": "^0.6.3", "@reduxjs/toolkit": "^1.9.5", + "@stripe/react-stripe-js": "^2.3.1", + "@stripe/stripe-js": "^2.1.9", "apng-js": "^1.1.1", "html-react-parser": "^3.0.16", "i18next": "^22.5.0", diff --git a/src/api/resources/Apps.ts b/src/api/resources/Apps.ts index 8cca9ea..970db05 100644 --- a/src/api/resources/Apps.ts +++ b/src/api/resources/Apps.ts @@ -17,6 +17,7 @@ export interface Response { first_open_subscription_popup: boolean runs_before_subscription_popup: number appirater_alerts: AppiraterAlertAppiraterAlert[] + stripe_public_key: string } } diff --git a/src/components/PaymentPage/index.tsx b/src/components/PaymentPage/index.tsx index d754abc..866dc0d 100644 --- a/src/components/PaymentPage/index.tsx +++ b/src/components/PaymentPage/index.tsx @@ -1,70 +1,87 @@ -import { useCallback, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { useDispatch, useSelector } from 'react-redux' -import { useNavigate } from 'react-router-dom' -import { selectors } from '@/store' -import { usePayment } from '@/payment' -import { actions } from '@/store' +import { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDispatch, useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { selectors } from "@/store"; +import { usePayment } from "@/payment"; +import { actions } from "@/store"; import { ApplePayBanner, ApplePayButton, GooglePayBanner, GooglePayButton, CardButton, - CardModal -} from './methods' -import ErrorModal from './ErrorModal' -import UserHeader from '../UserHeader' -import Title from '../Title' -import Loader from '../Loader' -import secure from './secure.png' -import routes from '@/routes' -import './styles.css' -import Header from '../Header' + CardModal, +} from "./methods"; +import ErrorModal from "./ErrorModal"; +import UserHeader from "../UserHeader"; +import Title from "../Title"; +import Loader from "../Loader"; +import secure from "./secure.png"; +import routes from "@/routes"; +import "./styles.css"; +import Header from "../Header"; +import { StripeButton, StripeModal } from "./methods/Stripe"; function PaymentPage(): JSX.Element { - const { t } = useTranslation() - const { applePay } = usePayment() - const [openCardModal, setOpenCardModal] = useState(false) - const [openErrorModal, setOpenErrorModal] = useState(false) - const dispatch = useDispatch() - const navigate = useNavigate() - const isLoading = applePay === null - const isApplePayAvailable = import.meta.env.PROD && applePay?.canMakePayments() - const email = useSelector(selectors.selectEmail) - const isDiscount = useSelector(selectors.selectIsDiscount) - const selectedPrice = useSelector(selectors.selectSelectedPrice) - const price = isDiscount ? (Math.round(selectedPrice || 0) / 2).toFixed(2) : selectedPrice + const { t } = useTranslation(); + const { applePay } = usePayment(); + const [openCardModal, setOpenCardModal] = useState(false); + const [openStripeModal, setOpenStripeModal] = useState(false); + const [openErrorModal, setOpenErrorModal] = useState(false); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const isLoading = applePay === null; + const isApplePayAvailable = + import.meta.env.PROD && applePay?.canMakePayments(); + const email = useSelector(selectors.selectEmail); + const isDiscount = useSelector(selectors.selectIsDiscount); + const selectedPrice = useSelector(selectors.selectSelectedPrice); + const price = isDiscount + ? (Math.round(selectedPrice || 0) / 2).toFixed(2) + : selectedPrice; const onSuccess = useCallback(() => { - dispatch(actions.status.update('subscribed')) - navigate(routes.client.wallpaper()) - }, [dispatch, navigate]) + dispatch(actions.status.update("subscribed")); + navigate(routes.client.wallpaper()); + }, [dispatch, navigate]); const onError = useCallback((error: Error) => { - console.error(error) - setOpenErrorModal(true) - }, []) + console.error(error); + setOpenErrorModal(true); + }, []); return ( <> -
navigate(routes.client.home())} /> +
navigate(routes.client.home())} + /> -
- { isLoading ? : ( +
+ {isLoading ? ( + + ) : ( <> -
- { isApplePayAvailable ? : } - 100% Secure +
+ {isApplePayAvailable ? : } + 100% Secure
- {t('choose_payment')} - { isApplePayAvailable - ? + + {t("choose_payment")} + + {/* {isApplePayAvailable ? ( - : - } -
{t('or').toUpperCase()}
- setOpenCardModal(true)} /> -

- {t('will_be_charged', { strongText: {t('trial_price', { price: price })} })} + ) : ( + + )} +

{t("or").toUpperCase()}
+ setOpenCardModal(true)} /> */} + setOpenStripeModal(true)} /> +

+ {t("will_be_charged", { + strongText: ( + {t("trial_price", { price: price })} + ), + })}

- setOpenErrorModal(false)} /> + setOpenStripeModal(false)} + onSuccess={onSuccess} + onError={onError} + /> + setOpenErrorModal(false)} + /> )}
- ) + ); } -export default PaymentPage +export default PaymentPage; diff --git a/src/components/PaymentPage/methods/Stripe/Button.tsx b/src/components/PaymentPage/methods/Stripe/Button.tsx new file mode 100644 index 0000000..2f35277 --- /dev/null +++ b/src/components/PaymentPage/methods/Stripe/Button.tsx @@ -0,0 +1,18 @@ + +import { useTranslation } from 'react-i18next' +import MainButton from '@/components/MainButton' +// import card from './card.svg' + +interface IStripeButtonProps { + onClick: () => void +} + +export function StripeButton({ onClick }: IStripeButtonProps): JSX.Element { + const { t } = useTranslation() + return ( + + {/* Credit / Debit Card */} + {t('stripe')} + + ) +} diff --git a/src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx b/src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx new file mode 100644 index 0000000..2a9dab2 --- /dev/null +++ b/src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx @@ -0,0 +1,56 @@ +import MainButton from "@/components/MainButton"; +import Title from "@/components/Title"; +import { + PaymentElement, + useElements, + useStripe, +} from "@stripe/react-stripe-js"; +import { useState } from "react"; + +export default function CheckoutForm() { + const stripe = useStripe(); + const elements = useElements(); + + const [message, setMessage] = useState(""); + const [isProcessing, setIsProcessing] = useState(false); + + const handleSubmit = async (e: any) => { + e.preventDefault(); + + if (!stripe || !elements) { + return; + } + + setIsProcessing(true); + + const { error, paymentIntent } = await stripe.confirmPayment({ + elements, + confirmParams: { + return_url: window.location.href, + }, + redirect: "if_required", + }); + + if (error) { + setMessage(error?.message || "Oops! Something went wrong."); + } else if (paymentIntent && paymentIntent.status === "succeeded") { + setMessage("Payment succeeded!"); + } else { + setMessage("Unexpected state"); + } + + setIsProcessing(false); + }; + + return ( +
+ + + + {isProcessing ? "Processing..." : "Pay now"} + + + {message} + + ); +} diff --git a/src/components/PaymentPage/methods/Stripe/Modal.tsx b/src/components/PaymentPage/methods/Stripe/Modal.tsx new file mode 100644 index 0000000..8a13d3b --- /dev/null +++ b/src/components/PaymentPage/methods/Stripe/Modal.tsx @@ -0,0 +1,74 @@ +import { SubscriptionReceipts, useApi } from "@/api"; +import Modal from "@/components/Modal"; +import Loader from "@/components/Loader"; +import { useEffect, useState } from "react"; +import { Stripe, loadStripe } from "@stripe/stripe-js"; +import { Elements } from "@stripe/react-stripe-js"; +import CheckoutForm from "./CheckoutForm"; + +interface StripeModalProps { + open: boolean; + onClose: () => void; + onSuccess: (receipt: SubscriptionReceipts.SubscriptionReceipt) => void; + onError: (error: Error) => void; +} + +export function StripeModal({ + open, + onClose, + onSuccess, + onError, +}: StripeModalProps): JSX.Element { + const api = useApi(); + const [stripePromise, setStripePromise] = + useState | null>(null); + const [clientSecret, setClientSecret] = useState(""); + const isLoading = false; + + useEffect(() => { + (async () => { + const siteConfig = await api.getAppConfig({ bundleId: "auraweb" }); + setStripePromise(loadStripe(siteConfig.data.stripe_public_key)); + })(); + }, [api]); + + useEffect(() => { + fetch("https://aura.wit.life/api/v1/user/subscription_receipts.json", { + method: "POST", + headers: { + Authorization: + "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjIzNjEyLCJpYXQiOjE2OTM0MTg5MTAsImV4cCI6MTcwMjA1ODkxMCwianRpIjoiNzg5MjkwYWItODg0YS00MGUyLTkyNjEtOWI2OGEyNjkwNmE0IiwiZW1haWwiOiJvdGhlckBleGFtcGxlLmNvbSIsInN0YXRlIjoicHJvdmVuIiwibG9jIjoiZW4iLCJ0eiI6LTI4ODAwLCJ0eXBlIjoiZW1haWwiLCJpc3MiOiJjb20ubGlmZS5hdXJhIn0.J2ocWIv5jKzuKMcwMgWMiNMyGg5qLlMAeln-bQm_9lw", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + way: "stripe", + subscription_receipt: { + item_interval: "year", + }, + }), + }).then(async (res) => { + const { subscription_receipt } = await res.json(); + const { client_secret } = subscription_receipt.data; + setClientSecret(client_secret); + }); + }, []); + + const handleClose = () => { + onClose(); + }; + + return ( + + {isLoading ? ( +
+ +
+ ) : null} + {stripePromise && clientSecret && ( + + + + )} +
+ ); +} diff --git a/src/components/PaymentPage/methods/Stripe/index.tsx b/src/components/PaymentPage/methods/Stripe/index.tsx new file mode 100644 index 0000000..794d8c4 --- /dev/null +++ b/src/components/PaymentPage/methods/Stripe/index.tsx @@ -0,0 +1,2 @@ +export * from './Button' +export * from './Modal' diff --git a/src/locales/dev.ts b/src/locales/dev.ts index ed18e08..d9a557b 100644 --- a/src/locales/dev.ts +++ b/src/locales/dev.ts @@ -80,6 +80,7 @@ export default { people_joined_today: " people joined today", you_and: "You and ", sign: "Sign", + stripe: "Stripe", 'aura-10_breath-button': "Increase up to 10%. Practice for the Energy of Money", 'aura-money_compatibility-button': "low MONEY energy. Determine who drains your energy", "breathe-subtitle": "Breathing practice will help improve your aura. Breath in the positive energy, breathe out the negative...",