feat: add stripe payment
This commit is contained in:
parent
336cc8ffba
commit
3d1361b57f
78
package-lock.json
generated
78
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -17,6 +17,7 @@ export interface Response {
|
||||
first_open_subscription_popup: boolean
|
||||
runs_before_subscription_popup: number
|
||||
appirater_alerts: AppiraterAlertAppiraterAlert[]
|
||||
stripe_public_key: string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
18
src/components/PaymentPage/methods/Stripe/Button.tsx
Normal file
18
src/components/PaymentPage/methods/Stripe/Button.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
56
src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx
Normal file
56
src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
74
src/components/PaymentPage/methods/Stripe/Modal.tsx
Normal file
74
src/components/PaymentPage/methods/Stripe/Modal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
2
src/components/PaymentPage/methods/Stripe/index.tsx
Normal file
2
src/components/PaymentPage/methods/Stripe/index.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './Button'
|
||||
export * from './Modal'
|
||||
@ -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...",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user