Payment on palmistry/discount page

This commit is contained in:
Евгений Пономарев 2024-03-24 21:14:13 +00:00 committed by Victor Ershov
parent d22157cde1
commit 974453cb74
12 changed files with 332 additions and 54 deletions

View File

@ -24,7 +24,7 @@ import {
SubscriptionPlans,
AppleAuth,
AIRequestsV2,
SinglePayment
SinglePayment,
} from './resources'
const api = {
@ -56,7 +56,6 @@ const api = {
getZodiacs: createMethod<Zodiacs.Payload, Zodiacs.Response>(Zodiacs.createRequest),
AIRequestsV2: createMethod<AIRequestsV2.Payload, AIRequestsV2.Response>(AIRequestsV2.createRequest),
getAIRequestsV2: createMethod<AIRequestsV2.PayloadGet, AIRequestsV2.IAiResponseGet>(AIRequestsV2.createRequestGet),
getSinglePaymentProducts: createMethod<SinglePayment.PayloadGet, SinglePayment.ResponseGet[]>(SinglePayment.createRequestGet),
createSinglePayment: createMethod<SinglePayment.PayloadPost, SinglePayment.ResponsePost | SinglePayment.ResponsePostExistPaymentData>(SinglePayment.createRequestPost),
}

View File

@ -15,12 +15,14 @@ interface ApplePayButtonProps {
activeSubPlan: ISubscriptionPlan | null;
client_secret: string;
subscriptionReceiptId?: string;
returnUrl?: string;
}
function ApplePayButton({
activeSubPlan,
client_secret,
subscriptionReceiptId,
returnUrl,
}: ApplePayButtonProps) {
const stripe = useStripe();
const elements = useElements();
@ -78,7 +80,7 @@ function ApplePayButton({
return e.complete("fail");
}
navigate(
`${routes.client.paymentResult()}/${subscriptionReceiptId}/?redirect_status=succeeded`
returnUrl || `${routes.client.paymentResult()}/${subscriptionReceiptId}/?redirect_status=succeeded`
);
e.complete("success");
// Show a success message to your customer

View File

@ -22,12 +22,11 @@ import PayPalButton from "./components/PayPalButton";
interface IPaymentModalProps {
activeSubscriptionPlan?: ISubscriptionPlan;
stripePublicKey?: string;
singlePayClientSecret?: string;
defaultPaymentMethod?: EPaymentMethod;
noTrial?: boolean;
returnUrl?: string;
}
function PaymentModal({ activeSubscriptionPlan }: IPaymentModalProps) {
function PaymentModal({ activeSubscriptionPlan, noTrial, returnUrl }: IPaymentModalProps) {
const { i18n } = useTranslation();
const locale = i18n.language;
const api = useApi();
@ -60,6 +59,9 @@ function PaymentModal({ activeSubscriptionPlan }: IPaymentModalProps) {
useEffect(() => {
(async () => {
const siteConfig = await api.getAppConfig({ bundleId: "auraweb" });
// const isProduction = import.meta.env.MODE === "production";
// const stripePublicKey = isProduction ? siteConfig.data.stripe_public_key : "pk_test_51Ndqf4IlX4lgwUxrlLWqfYWpo0Ic0BV7DfiZxfMYy838IZP8NLrwwZ5i0HhhbOQBGoQZe4Rrel1ziEk8mhQ2TE3500ETWZPBva";
// setStripePromise(loadStripe(stripePublicKey));
setStripePromise(loadStripe(siteConfig.data.stripe_public_key));
const { sub_plans } = await api.getSubscriptionPlans({ locale });
setSubPlans(sub_plans);
@ -173,22 +175,27 @@ function PaymentModal({ activeSubscriptionPlan }: IPaymentModalProps) {
/>
{activeSubPlan && (
<div>
<p className={styles["sub-plan-description"]}>
You will be charged only{" "}
<b>
${getPriceFromTrial(activeSubPlan?.trial)} for your 3-day trial.
</b>
</p>
<p className={styles["sub-plan-description"]}>
We`ll <b>email you a reminder</b> before your trial period ends.
</p>
{!noTrial && (
<>
<p className={styles["sub-plan-description"]}>
You will be charged only{" "}
<b>
${getPriceFromTrial(activeSubPlan?.trial)} for your 3-day trial.
</b>
</p>
<p className={styles["sub-plan-description"]}>
We`ll <b>email you a reminder</b> before your trial period ends.
</p>
</>
)}
<p className={styles["sub-plan-description"]}>
Cancel anytime. The charge will appear on your bill as witapps.
</p>
</div>
)}
<div className={styles["payment-method-container"]}>
{stripePromise && clientSecret && subscriptionReceiptId && (
{stripePromise && clientSecret && (
<Elements stripe={stripePromise} options={{ clientSecret }}>
{selectedPaymentMethod === EPaymentMethod.PAYPAL_OR_APPLE_PAY && (
<div className={styles["payment-method"]}>
@ -202,13 +209,14 @@ function PaymentModal({ activeSubscriptionPlan }: IPaymentModalProps) {
activeSubPlan={activeSubPlan}
client_secret={clientSecret}
subscriptionReceiptId={subscriptionReceiptId}
returnUrl={window.location.href}
/>
{!!errors.length && <p className={styles.errors}>{errors}</p>}
</div>
)}
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
<CheckoutForm subscriptionReceiptId={subscriptionReceiptId} />
<CheckoutForm subscriptionReceiptId={subscriptionReceiptId} returnUrl={returnUrl} />
)}
</Elements>
)}

View File

@ -58,6 +58,11 @@
border: 2px solid #c7c7c7;
}
.discount-screen__block:first-child .discount-screen__button {
background: #c7c7c7;
color: #000;
}
.discount-screen__block:last-child {
padding-top: 0;
border: 2px solid #066fde;
@ -111,3 +116,47 @@
width: calc(100% + 32px);
justify-content: center;
}
.discount-screen__widget {
background: #fff;
bottom: 0;
box-shadow: 0 -2px 16px rgba(18, 22, 32, .1);
max-width: 428px;
width: 100%;
padding: 40px;
position: relative;
}
.discount-screen__widget_success {
height: 400px;
}
.discount-screen__success {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background: #fff;
z-index: 99;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 30px;
padding: 40px;
}
.discount-screen__success-icon {
width: 100px;
height: 100px;
max-width: 50%;
flex-shrink: 0;
}
.discount-screen__success-text {
font-size: 24px;
line-height: 32px;
text-align: center;
color: #121620;
}

View File

@ -1,26 +1,98 @@
import React from "react";
import { useNavigate } from 'react-router-dom';
import { useTranslation } from "react-i18next";
import { Elements } from "@stripe/react-stripe-js";
import { Stripe, loadStripe } from "@stripe/stripe-js";
import './discount-screen.css';
import routes from '@/routes';
import { useApi } from "@/api";
import { useAuth } from "@/auth";
import HeaderLogo from '@/components/palmistry/header-logo/header-logo';
import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm";
const currentProductId = "prod_PkXPacpzM8jSmE";
export default function DiscountScreen() {
const navigate = useNavigate();
const api = useApi();
const { token, user } = useAuth();
const { i18n } = useTranslation();
const locale = i18n.language;
const userHasWeeklySubscription = false;
const [price, setPrice] = React.useState('');
const [isSuccess] = React.useState(false);
const [stripePromise, setStripePromise] = React.useState<Promise<Stripe | null> | null>(null);
const [productId, setProductId] = React.useState('');
const [clientSecret, setClientSecret] = React.useState<string | null>(null);
const [stripePublicKey, setStripePublicKey] = React.useState<string>("");
const goPremiumBundle = () => {
navigate(routes.client.palmistryPremiumBundle());
};
React.useEffect(() => {
if (userHasWeeklySubscription) {
(async () => {
const { sub_plans } = await api.getSubscriptionPlans({ locale });
const plan = sub_plans.find((plan) => plan.id === "stripe.40");
if (!plan?.price_cents) return;
setPrice((plan?.price_cents / 100).toFixed(2));
})();
}, []);
React.useEffect(() => {
(async () => {
const products = await api.getSinglePaymentProducts({ token });
const product = products.find((product) => product.productId === currentProductId);
if (product) {
setProductId(product.productId);
}
})();
}, []);
React.useEffect(() => {
if (!stripePublicKey) return;
setStripePromise(loadStripe(stripePublicKey));
}, [stripePublicKey]);
const buy = async () => {
if (!user?.id) return;
const response = await api.createSinglePayment({
token: token,
data: {
user: {
id: user.id,
email: user.email,
name: user.username || "",
sign: user.profile?.sign?.sign || "",
age: user.profile.age?.years || 0,
},
partner: {
sign: "",
age: 0,
},
paymentInfo: {
productId,
},
return_url: `${window.location.host}/palmistry/premium-bundle`,
},
});
if ('paymentIntent' in response && response.paymentIntent.status === "paid" || 'payment' in response && response.payment.status === "paid") {
goPremiumBundle();
} else if ('paymentIntent' in response) {
setClientSecret(response.paymentIntent.data.client_secret);
setStripePublicKey(response.paymentIntent.data.public_key);
}
}, [userHasWeeklySubscription]);
};
return (
<div className="discount-screen">
@ -55,7 +127,7 @@ export default function DiscountScreen() {
<section className="discount-screen__block">
<div className="discount-screen__header-block">save 33%</div>
<span className="discount-screen__price-block">12.73 for <br /> 1-week plan</span>
<span className="discount-screen__price-block">{price} for <br /> 1-week plan</span>
<div className="discount-screen__details">
<span className="discount-screen__details-name">Total savings</span>
@ -67,12 +139,39 @@ export default function DiscountScreen() {
<span className="discount-screen__details-value">no</span>
</div>
<button className="discount-screen__button">
<button className="discount-screen__button" onClick={buy}>
Pay now and <br /> skip trial
</button>
</section>
</div>
</div>
{stripePromise && clientSecret && (
<div className={`discount-screen__widget${isSuccess ? " discount-screen__widget_success" : ""}`}>
<Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm returnUrl={`${window.location.host}/palmistry/premium-bundle`} />
</Elements>
{isSuccess && (
<div className="discount-screen__success">
<svg
className="discount-screen__success-icon"
xmlns="http://www.w3.org/2000/svg"
width="512"
height="512"
viewBox="0 0 52 52"
>
<path
fill="#4ec794"
d="M26 0C11.664 0 0 11.663 0 26s11.664 26 26 26 26-11.663 26-26S40.336 0 26 0zm14.495 17.329-16 18a1.997 1.997 0 0 1-2.745.233l-10-8a2 2 0 0 1 2.499-3.124l8.517 6.813L37.505 14.67a2.001 2.001 0 0 1 2.99 2.659z"
/>
</svg>
<div className="discount-screen__success-text">Payment success</div>
</div>
)}
</div>
)}
</div>
);
}

View File

@ -1,11 +1,9 @@
import React from "react";
import { useSelector } from "react-redux";
import { useNavigate } from 'react-router-dom';
import './payment-screen.css';
import routes from '@/routes';
import useSteps, { Step } from '@/hooks/palmistry/use-steps';
import useTimer from '@/hooks/palmistry/use-timer';
import HeaderLogo from '@/components/palmistry/header-logo/header-logo';
@ -17,18 +15,16 @@ const getFormattedPrice = (price: number) => {
}
export default function PaymentScreen() {
const navigate = useNavigate();
const time = useTimer();
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan);
// const subscriptionStatus = useSelector(selectors.selectStatus);
const subscriptionStatus = "subscribed";
const subscriptionStatus = useSelector(selectors.selectStatus);
const steps = useSteps();
React.useEffect(() => {
if (subscriptionStatus === "subscribed") {
setTimeout(() => {
navigate(routes.client.palmistryDiscount());
steps.goNext();
}, 1500);
}
}, [subscriptionStatus]);
@ -242,7 +238,7 @@ export default function PaymentScreen() {
{activeSubPlanFromStore && (
<div className={`payment-screen__widget${subscriptionStatus === "subscribed" ? " payment-screen__widget_success" : ""}`}>
{subscriptionStatus !== "subscribed" && <PaymentModal />}
{subscriptionStatus !== "subscribed" && <PaymentModal returnUrl={window.location.href}/>}
{subscriptionStatus === "subscribed" && (
<div className="payment-screen__success">

View File

@ -137,3 +137,47 @@
fill: #fff;
margin-right: 8px;
}
.premium-bundle-screen__widget {
background: #fff;
bottom: 0;
box-shadow: 0 -2px 16px rgba(18, 22, 32, .1);
max-width: 428px;
width: 100%;
padding: 40px;
position: relative;
}
.premium-bundle-screen__widget_success {
height: 400px;
}
.premium-bundle-screen__success {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background: #fff;
z-index: 99;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 30px;
padding: 40px;
}
.premium-bundle-screen__success-icon {
width: 100px;
height: 100px;
max-width: 50%;
flex-shrink: 0;
}
.premium-bundle-screen__success-text {
font-size: 24px;
line-height: 32px;
text-align: center;
color: #121620;
}

View File

@ -1,22 +1,79 @@
import React from "react";
import { useNavigate } from 'react-router-dom';
import { Elements } from "@stripe/react-stripe-js";
import { Stripe, loadStripe } from "@stripe/stripe-js";
import './premium-bundle-screen.css';
import routes from '@/routes';
import HeaderLogo from '@/components/palmistry/header-logo/header-logo';
import { useApi } from '@/api';
import { useAuth } from "@/auth";
import CheckoutForm from "@/components/PaymentPage/methods/Stripe/CheckoutForm";
const currentProductId = "prod_PkXPacpzM8jSmE";
export default function PremiumBundleScreen() {
const navigate = useNavigate();
const { token, user } = useAuth();
const api = useApi();
const userHasPremiumBundle = false;
const [stripePromise, setStripePromise] = React.useState<Promise<Stripe | null> | null>(null);
const [productId, setProductId] = React.useState('');
const [isSuccess] = React.useState(false);
const [clientSecret, setClientSecret] = React.useState<string | null>(null);
const [stripePublicKey, setStripePublicKey] = React.useState<string>("");
React.useEffect(() => {
if (userHasPremiumBundle) {
navigate(routes.client.home());
(async () => {
const products = await api.getSinglePaymentProducts({ token });
const product = products.find((product) => product.productId === currentProductId);
if (product) {
setProductId(product.productId);
}
})();
}, []);
React.useEffect(() => {
if (!stripePublicKey) return;
setStripePromise(loadStripe(stripePublicKey));
}, [stripePublicKey]);
const buy = async () => {
if (!user?.id) return;
const response = await api.createSinglePayment({
token: token,
data: {
user: {
id: user.id,
email: user.email,
name: user.username || "",
sign: user.profile?.sign?.sign || "",
age: user.profile.age?.years || 0,
},
partner: {
sign: "",
age: 0,
},
paymentInfo: {
productId,
},
return_url: `${window.location.host}/palmistry/premium-bundle`,
},
});
if ('paymentIntent' in response && response.paymentIntent.status === "paid" || 'payment' in response && response.payment.status === "paid") {
goHome();
} else if ('paymentIntent' in response) {
setClientSecret(response.paymentIntent.data.client_secret);
setStripePublicKey(response.paymentIntent.data.public_key);
}
}, [userHasPremiumBundle]);
};
const goHome = () => {
navigate(routes.client.home());
@ -134,7 +191,10 @@ export default function PremiumBundleScreen() {
</div>
</div>
<button className="premium-bundle-screen__button premium-bundle-screen__button-active premium-bundle-screen__buy-button">
<button
className="premium-bundle-screen__button premium-bundle-screen__button-active premium-bundle-screen__buy-button"
onClick={buy}
>
<svg
width="13"
height="16"
@ -151,6 +211,33 @@ export default function PremiumBundleScreen() {
Buy now
</button>
</div>
{stripePromise && clientSecret && (
<div className={`discount-screen__widget${isSuccess ? " discount-screen__widget_success" : ""}`}>
<Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm returnUrl={window.location.host} />
</Elements>
{isSuccess && (
<div className="discount-screen__success">
<svg
className="discount-screen__success-icon"
xmlns="http://www.w3.org/2000/svg"
width="512"
height="512"
viewBox="0 0 52 52"
>
<path
fill="#4ec794"
d="M26 0C11.664 0 0 11.663 0 26s11.664 26 26 26 26-11.663 26-26S40.336 0 26 0zm14.495 17.329-16 18a1.997 1.997 0 0 1-2.745.233l-10-8a2 2 0 0 1 2.499-3.124l8.517 6.813L37.505 14.67a2.001 2.001 0 0 1 2.99 2.659z"
/>
</svg>
<div className="discount-screen__success-text">Payment success</div>
</div>
)}
</div>
)}
</div>
);
}

View File

@ -2,6 +2,7 @@ import React from 'react';
import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import { PatchPayload } from "@/api/resources/User";
import { Step } from '@/hooks/palmistry/use-steps';
import { useAuth } from "@/auth";
import { useApi, ApiError, extractErrorMessage } from "@/api";
@ -55,18 +56,23 @@ export default function StepEmail() {
setIsLoading(true);
const auth = await api.auth({ email, timezone, locale });
const { auth: { token, user } } = auth;
signUp(token, user);
const payload = {
const payload: PatchPayload = {
user: {
profile_attributes: {
birthday: steps.getStoredValue(Step.Birthdate),
gender: steps.getStoredValue(Step.Gender),
relationship_status: steps.getStoredValue(Step.RelationshipStatus),
},
},
token,
};
const relationshipStatus = steps.getStoredValue(Step.RelationshipStatus);
if (relationshipStatus) {
payload.user.profile_attributes!.relationship_status = relationshipStatus;
}
const updatedUser = await api.updateUser(payload).catch((error) => console.log("Error: ", error));
if (updatedUser?.user) dispatch(actions.user.update(updatedUser.user));

View File

@ -1,13 +1,10 @@
import React from 'react';
import { useNavigate } from "react-router-dom";
import { Step } from '@/hooks/palmistry/use-steps';
import useSteps from '@/hooks/palmistry/use-steps';
import Paywall from '@/components/palmistry/paywall/paywall';
export default function StepPaywall() {
const navigate = useNavigate();
const steps = useSteps();
const storedEmail = steps.getStoredValue(Step.Email);
@ -18,7 +15,7 @@ export default function StepPaywall() {
}, [storedEmail]);
const onNext = () => {
navigate('/palmistry/payment');
steps.goNext();
};
return (

View File

@ -1,13 +1,9 @@
import React from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { useLocation } from 'react-router-dom';
import { useSelector } from "react-redux";
import { useNavigate } from 'react-router-dom';
import './steps-manager.css';
import routes from '@/routes';
import { selectors } from "@/store";
import Progressbar from '@/components/palmistry/progress-bar/progress-bar';
import PalmistryContainer from '@/components/palmistry/palmistry-container/palmistry-container';
import useSteps, { Step } from '@/hooks/palmistry/use-steps';
@ -52,8 +48,6 @@ const animationDuration = 0.2;
export default function StepsManager() {
const steps = useSteps();
const { pathname } = useLocation();
const subscriptionStatus = useSelector(selectors.selectStatus);
const navigate = useNavigate();
const [modalIsOpen, setModalIsOpen] = React.useState(false);
@ -62,12 +56,6 @@ export default function StepsManager() {
steps.goFirstUnpassedStep();
}, [steps.isInited]);
React.useEffect(() => {
if (subscriptionStatus === "subscribed" && steps.current !== Step.Payment) {
navigate(routes.client.home());
}
}, [subscriptionStatus]);
const motionDivClassName = [
'steps-manager__motion-div',

View File

@ -1,7 +1,10 @@
import type { UserStatus } from "./types";
const isProduction = import.meta.env.MODE === "production";
const host = "";
export const apiHost = "https://api-web.aura.wit.life";
const dApiHost = isProduction ? "https://d.api.witapps.us" : "https://dev.api.witapps.us"
const siteHost = "https://aura.wit.life";
const prefix = "api/v1";
const dApiHost = "https://dev.api.witapps.us";
@ -176,7 +179,6 @@ const routes = {
),
getAiRequestsV2: (id: string) =>
[apiHost, "api/v2", "ai", "requests", `${id}.json`].join("/"),
dApiTestPaymentProducts: () =>
[dApiHost, "payment", "test", "products"].join("/"),
dApiPaymentCheckout: () => [dApiHost, "payment", "checkout"].join("/"),
@ -276,6 +278,7 @@ export const withoutFooterRoutes = [
routes.client.trialPaymentWithDiscount(),
routes.client.palmistryPaywall(),
routes.client.palmistryPayment(),
routes.client.palmistryDiscount(),
routes.client.email("marketing-landing"),
routes.client.email("marketing-trial-payment"),
routes.client.tryApp(),