feat: add stripe payment

This commit is contained in:
gofnnp 2023-10-19 03:10:27 +04:00
parent 336cc8ffba
commit 3d1361b57f
9 changed files with 311 additions and 53 deletions

78
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -17,6 +17,7 @@ export interface Response {
first_open_subscription_popup: boolean
runs_before_subscription_popup: number
appirater_alerts: AppiraterAlertAppiraterAlert[]
stripe_public_key: string
}
}

View File

@ -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 (
<>
<Header showCross={true} clickCross={() => navigate(routes.client.home())} />
<Header
showCross={true}
clickCross={() => navigate(routes.client.home())}
/>
<UserHeader email={email} />
<section className='page'>
{ isLoading ? <Loader /> : (
<section className="page">
{isLoading ? (
<Loader />
) : (
<>
<div className='page-header'>
{ isApplePayAvailable ? <ApplePayBanner /> : <GooglePayBanner /> }
<img src={secure} alt='100% Secure' />
<div className="page-header">
{isApplePayAvailable ? <ApplePayBanner /> : <GooglePayBanner />}
<img src={secure} alt="100% Secure" />
</div>
<Title variant='h1' className='mb-45'>{t('choose_payment')}</Title>
{ isApplePayAvailable
?
<Title variant="h1" className="mb-45">
{t("choose_payment")}
</Title>
{/* {isApplePayAvailable ? (
<ApplePayButton onSuccess={onSuccess} onError={onError} />
:
<GooglePayButton onSuccess={onSuccess} onError={onError} /> }
<div className='payment-divider'>{t('or').toUpperCase()}</div>
<CardButton onClick={() => setOpenCardModal(true)} />
<p className='payment-warining'>
{t('will_be_charged', { strongText: <strong>{t('trial_price', { price: price })}</strong> })}
) : (
<GooglePayButton onSuccess={onSuccess} onError={onError} />
)}
<div className="payment-divider">{t("or").toUpperCase()}</div>
<CardButton onClick={() => setOpenCardModal(true)} /> */}
<StripeButton onClick={() => setOpenStripeModal(true)} />
<p className="payment-warining">
{t("will_be_charged", {
strongText: (
<strong>{t("trial_price", { price: price })}</strong>
),
})}
</p>
<CardModal
open={openCardModal}
@ -72,12 +89,21 @@ function PaymentPage(): JSX.Element {
onSuccess={onSuccess}
onError={onError}
/>
<ErrorModal open={openErrorModal} onClose={() => setOpenErrorModal(false)} />
<StripeModal
open={openStripeModal}
onClose={() => setOpenStripeModal(false)}
onSuccess={onSuccess}
onError={onError}
/>
<ErrorModal
open={openErrorModal}
onClose={() => setOpenErrorModal(false)}
/>
</>
)}
</section>
</>
)
);
}
export default PaymentPage
export default PaymentPage;

View File

@ -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 (
<MainButton color='blue' onClick={onClick}>
{/* <img className='payment-card' src={card} alt='Credit / Debit Card' /> */}
{t('stripe')}
</MainButton>
)
}

View File

@ -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<boolean>(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 (
<form id="payment-form" onSubmit={handleSubmit}>
<PaymentElement />
<MainButton color="blue" disabled={isProcessing} id="submit">
<span id="button-text">
{isProcessing ? "Processing..." : "Pay now"}
</span>
</MainButton>
<Title variant="h4">{message}</Title>
</form>
);
}

View File

@ -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<Promise<Stripe | null> | null>(null);
const [clientSecret, setClientSecret] = useState<string>("");
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 (
<Modal open={open} onClose={handleClose}>
{isLoading ? (
<div className="payment-loader">
<Loader />
</div>
) : null}
{stripePromise && clientSecret && (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm />
</Elements>
)}
</Modal>
);
}

View File

@ -0,0 +1,2 @@
export * from './Button'
export * from './Modal'

View File

@ -80,6 +80,7 @@ export default {
people_joined_today: "<countPeoples> people joined today",
you_and: "You and <user>",
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...",