AW-104-express-checkout

This commit is contained in:
Денис Катаев 2024-06-12 23:15:41 +00:00 committed by Daniil Chemerkin
parent fb00697993
commit d8da6d9de6
8 changed files with 345 additions and 151 deletions

BIN
public/amazon-pay-mark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,116 @@
import { useMemo, useState } from "react";
import styles from "./styles.module.css";
import {
useStripe,
useElements,
ExpressCheckoutElement,
} from "@stripe/react-stripe-js";
import {
AvailablePaymentMethods,
StripeExpressCheckoutElementReadyEvent,
} from "@stripe/stripe-js";
interface IExpressCheckoutStripeProps {
clientSecret: string;
returnUrl?: string;
isHide?: boolean;
onAvailable?: (
isAvailable: boolean,
availableMethods: AvailablePaymentMethods | undefined
) => void;
onChangeLoading?: (isLoading: boolean) => void;
}
const checkFormAvailable = (
availablePaymentMethods: undefined | AvailablePaymentMethods
) => {
if (!availablePaymentMethods) return false;
let result = false;
for (const key in availablePaymentMethods) {
if (availablePaymentMethods[key as keyof AvailablePaymentMethods]) {
result = true;
break;
}
}
return result;
};
function ExpressCheckoutStripe({
clientSecret,
returnUrl = "https://${window.location.host}/payment/result/",
isHide = false,
onAvailable,
onChangeLoading,
}: IExpressCheckoutStripeProps) {
const stripe = useStripe();
const elements = useElements();
const [errorMessage, setErrorMessage] = useState<string>();
const [isAvailable, setIsAvailable] = useState(false);
const isHideForm = useMemo(
() => isHide || !isAvailable,
[isAvailable, isHide]
);
const onConfirm = async () =>
// event: StripeExpressCheckoutElementConfirmEvent
{
if (!stripe || !elements) {
// Stripe.js hasn't loaded yet.
// Make sure to disable form submission until Stripe.js has loaded.
return;
}
const { error: submitError } = await elements.submit();
if (submitError) {
setErrorMessage(submitError.message);
return;
}
// // Create the PaymentIntent and obtain clientSecret
// const res = await fetch("/create-intent", {
// method: "POST",
// });
// const { client_secret: clientSecret } = await res.json();
// Confirm the PaymentIntent using the details collected by the Express Checkout Element
const { error } = await stripe.confirmPayment({
// `elements` instance used to create the Express Checkout Element
elements,
// `clientSecret` from the created PaymentIntent
clientSecret,
confirmParams: {
return_url: returnUrl,
},
});
if (error) {
// This point is only reached if there's an immediate error when
// confirming the payment. Show the error to your customer (for example, payment details incomplete)
setErrorMessage(error.message);
} else {
// The payment UI automatically closes with a success animation.
// Your customer is redirected to your `return_url`.
}
};
const onReady = (event: StripeExpressCheckoutElementReadyEvent) => {
const _isAvailable = checkFormAvailable(event.availablePaymentMethods);
setIsAvailable(_isAvailable);
onAvailable && onAvailable(_isAvailable, event.availablePaymentMethods);
onChangeLoading && onChangeLoading(false);
};
return (
<div className={`${styles.container} ${isHideForm ? styles.hide : ""}`}>
<ExpressCheckoutElement
onReady={onReady}
onLoadError={() => onChangeLoading && onChangeLoading(false)}
onConfirm={onConfirm}
/>
{errorMessage && <p className={styles.error}>{errorMessage}</p>}
</div>
);
}
export default ExpressCheckoutStripe;

View File

@ -0,0 +1,16 @@
.container {
width: 100%;
}
.hide {
height: 0;
visibility: hidden;
}
.error {
width: 100%;
color: #FF5758;
text-align: center;
font-size: 14px;
margin-top: 8px;
}

View File

@ -5,7 +5,7 @@ import { useEffect, useMemo, useState } from "react";
import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods";
import { Elements } from "@stripe/react-stripe-js";
import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm";
import { Stripe, loadStripe } from "@stripe/stripe-js";
import { AvailablePaymentMethods, Stripe, loadStripe } from "@stripe/stripe-js";
import { useSelector } from "react-redux";
import { selectors } from "@/store";
import { useNavigate } from "react-router-dom";
@ -15,8 +15,7 @@ import SecurityPayments from "../SecurityPayments";
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import { useMakePayment } from "@/hooks/payment/useMakePayment";
import StripeButton from "@/components/PaymentPage/methods/StripeButton";
import CheckAvailableStripeButton from "@/components/PaymentPage/methods/StripeButton/CheckAvailableStripeButton";
import ExpressCheckoutStripe from "@/components/PaymentPage/methods/ExpressCheckoutStripe";
interface IPaymentModalProps {
activeProduct?: IPaywallProduct;
@ -49,7 +48,7 @@ function PaymentModal({
returnUrl: checkoutUrl,
paymentType,
publicKey,
isLoading,
isLoading: isLoadingPayment,
error,
} = useMakePayment({
productId: _activeProduct?._id || "",
@ -58,17 +57,23 @@ function PaymentModal({
returnPaidUrl: returnUrl,
});
const stripeButton = useSelector(selectors.selectStripeButton);
const [availableMethods, setAvailableMethods] = useState<
AvailablePaymentMethods | undefined
>();
const paymentRequest = stripeButton?.paymentRequest;
const availableMethods = stripeButton?.availableMethods;
const [isLoadingExpressCheckout, setIsLoadingExpressCheckout] =
useState(true);
const isLoading = useMemo(() => {
return isLoadingPayment || isLoadingExpressCheckout;
}, [isLoadingPayment, isLoadingExpressCheckout]);
if (checkoutUrl?.length) {
window.location.href = checkoutUrl;
}
const paymentMethodsButtons = useMemo(() => {
return paymentMethods(availableMethods);
return paymentMethods(availableMethods || null);
}, [availableMethods]);
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
@ -92,15 +97,16 @@ function PaymentModal({
})();
}, [_activeProduct, navigate, products, publicKey]);
if (isLoading) {
return (
<div className={styles["payment-modal"]}>
<div className={styles["payment-loader"]}>
<Loader />
</div>
</div>
);
}
const onAvailableExpressCheckout = (
isAvailable: boolean,
availableMethods: AvailablePaymentMethods | undefined
) => {
if (isAvailable && availableMethods) {
setAvailableMethods(availableMethods);
return setSelectedPaymentMethod(EPaymentMethod.PAYMENT_BUTTONS);
}
return setAvailableMethods(undefined);
};
if (error?.length) {
return (
@ -113,62 +119,75 @@ function PaymentModal({
}
return (
<div className={styles["payment-modal"]}>
<Title variant="h3" className={styles.title}>
Choose payment method
</Title>
<PaymentMethodsChoice
paymentMethods={paymentMethodsButtons}
selectedPaymentMethod={selectedPaymentMethod}
onSelectPaymentMethod={onSelectPaymentMethod}
/>
{_activeProduct && (
<div>
{!noTrial && (
<>
<p className={styles["sub-plan-description"]}>
You will be charged only{" "}
<b>${getPrice(_activeProduct)} 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>
<>
{isLoading && (
<div className={styles["payment-modal"]}>
<div className={styles["payment-loader"]}>
<Loader />
</div>
</div>
)}
<div className={styles["payment-method-container"]}>
{stripePromise && clientSecret && (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckAvailableStripeButton
activeProduct={_activeProduct}
clientSecret={clientSecret}
/>
{selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && (
<div className={styles["payment-method"]}>
{paymentRequest && (
<StripeButton paymentRequest={paymentRequest} />
)}
</div>
<div
className={`${styles["payment-modal"]} ${isLoading ? styles.hide : ""}`}
>
<Title variant="h3" className={styles.title}>
Choose payment method
</Title>
<PaymentMethodsChoice
paymentMethods={paymentMethodsButtons}
selectedPaymentMethod={selectedPaymentMethod}
onSelectPaymentMethod={onSelectPaymentMethod}
/>
{_activeProduct && (
<div>
{!noTrial && (
<>
<p className={styles["sub-plan-description"]}>
You will be charged only{" "}
<b>${getPrice(_activeProduct)} 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>
</>
)}
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
<CheckoutForm
confirmType={paymentType}
subscriptionReceiptId={paymentIntentId}
returnUrl={returnUrl}
/>
)}
</Elements>
<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 && (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<ExpressCheckoutStripe
clientSecret={clientSecret}
returnUrl={returnUrl}
isHide={
selectedPaymentMethod !== EPaymentMethod.PAYMENT_BUTTONS
}
onAvailable={(_isAvailable, _availableMethods) =>
onAvailableExpressCheckout(_isAvailable, _availableMethods)
}
onChangeLoading={(isLoading) =>
setIsLoadingExpressCheckout(isLoading)
}
/>
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
<CheckoutForm
confirmType={paymentType}
subscriptionReceiptId={paymentIntentId}
returnUrl={returnUrl}
/>
)}
</Elements>
)}
</div>
<SecurityPayments />
<p className={styles.address}>500 N RAINBOW BLVD LAS VEGAS, NV 89107</p>
</div>
<SecurityPayments />
<p className={styles.address}>500 N RAINBOW BLVD LAS VEGAS, NV 89107</p>
</div>
</>
);
}

View File

@ -8,6 +8,12 @@
color: #2f2e37;
}
.payment-modal.hide {
min-height: 0;
height: 0;
opacity: 0;
}
.title {
font-weight: 700;
font-size: 20px;

View File

@ -5,7 +5,7 @@ import { useEffect, useMemo, useState } from "react";
import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods";
import { Elements } from "@stripe/react-stripe-js";
import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm";
import { Stripe, loadStripe } from "@stripe/stripe-js";
import { AvailablePaymentMethods, Stripe, loadStripe } from "@stripe/stripe-js";
import { useSelector } from "react-redux";
import { selectors } from "@/store";
import Loader from "@/components/Loader";
@ -13,8 +13,9 @@ import SecurityPayments from "../SecurityPayments";
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import { useMakePayment } from "@/hooks/payment/useMakePayment";
import StripeButton from "@/components/PaymentPage/methods/StripeButton";
import CheckAvailableStripeButton from "@/components/PaymentPage/methods/StripeButton/CheckAvailableStripeButton";
import ExpressCheckoutStripe from "@/components/PaymentPage/methods/ExpressCheckoutStripe";
import routes from "@/routes";
import { useNavigate } from "react-router-dom";
interface IPaymentModalProps {
activeProduct?: IPaywallProduct;
@ -36,6 +37,7 @@ function PaymentModal({
returnUrl,
placementKey = EPlacementKeys["aura.placement.main"],
}: IPaymentModalProps) {
const navigate = useNavigate();
const [stripePromise, setStripePromise] =
useState<Promise<Stripe | null> | null>(null);
@ -51,7 +53,7 @@ function PaymentModal({
returnUrl: checkoutUrl,
paymentType,
publicKey,
isLoading,
isLoading: isLoadingPayment,
error,
} = useMakePayment({
productId: _activeProduct?._id || "",
@ -60,20 +62,27 @@ function PaymentModal({
returnPaidUrl: returnUrl,
});
const { paymentRequest, availableMethods } = useSelector(
selectors.selectStripeButton
);
const [availableMethods, setAvailableMethods] = useState<
AvailablePaymentMethods | undefined
>();
const [isLoadingExpressCheckout, setIsLoadingExpressCheckout] =
useState(true);
const isLoading = useMemo(() => {
return isLoadingPayment || isLoadingExpressCheckout;
}, [isLoadingPayment, isLoadingExpressCheckout]);
if (checkoutUrl?.length) {
window.location.href = checkoutUrl;
}
const paymentMethodsButtons = useMemo(() => {
return paymentMethods(availableMethods);
return paymentMethods(availableMethods || null);
}, [availableMethods]);
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
EPaymentMethod.PAYMENT_BUTTONS
paymentMethodsButtons[0].id
);
const onSelectPaymentMethod = (method: EPaymentMethod) => {
@ -84,18 +93,25 @@ function PaymentModal({
(async () => {
if (!products?.length || !publicKey) return;
setStripePromise(loadStripe(publicKey));
const isActiveProduct = products.find(
(product) => product._id === _activeProduct?._id
);
if (!_activeProduct || !isActiveProduct) {
navigate(routes.client.trialChoice());
}
})();
}, [products, publicKey]);
}, [_activeProduct, navigate, products, publicKey]);
if (isLoading) {
return (
<div className={styles["payment-modal"]}>
<div className={styles["payment-loader"]}>
<Loader />
</div>
</div>
);
}
const onAvailableExpressCheckout = (
isAvailable: boolean,
availableMethods: AvailablePaymentMethods | undefined
) => {
if (isAvailable && availableMethods) {
setAvailableMethods(availableMethods);
return setSelectedPaymentMethod(EPaymentMethod.PAYMENT_BUTTONS);
}
return setAvailableMethods(undefined);
};
if (error?.length) {
return (
@ -108,62 +124,75 @@ function PaymentModal({
}
return (
<div className={styles["payment-modal"]}>
<Title variant="h3" className={styles.title}>
Choose payment method
</Title>
<PaymentMethodsChoice
paymentMethods={paymentMethodsButtons}
selectedPaymentMethod={selectedPaymentMethod}
onSelectPaymentMethod={onSelectPaymentMethod}
/>
{_activeProduct && (
<div>
{!noTrial && (
<>
<p className={styles["sub-plan-description"]}>
You will be charged only{" "}
<b>${getPrice(_activeProduct)} 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>
<>
{isLoading && (
<div className={styles["payment-modal"]}>
<div className={styles["payment-loader"]}>
<Loader />
</div>
</div>
)}
<div className={styles["payment-method-container"]}>
{stripePromise && clientSecret && (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckAvailableStripeButton
activeProduct={_activeProduct}
clientSecret={clientSecret}
/>
{selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && (
<div className={styles["payment-method"]}>
{paymentRequest && (
<StripeButton paymentRequest={paymentRequest} />
)}
</div>
<div
className={`${styles["payment-modal"]} ${isLoading ? styles.hide : ""}`}
>
<Title variant="h3" className={styles.title}>
Choose payment method
</Title>
<PaymentMethodsChoice
paymentMethods={paymentMethodsButtons}
selectedPaymentMethod={selectedPaymentMethod}
onSelectPaymentMethod={onSelectPaymentMethod}
/>
{_activeProduct && (
<div>
{!noTrial && (
<>
<p className={styles["sub-plan-description"]}>
You will be charged only{" "}
<b>${getPrice(_activeProduct)} 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>
</>
)}
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
<CheckoutForm
confirmType={paymentType}
subscriptionReceiptId={paymentIntentId}
returnUrl={returnUrl}
/>
)}
</Elements>
<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 && (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<ExpressCheckoutStripe
clientSecret={clientSecret}
returnUrl={returnUrl}
isHide={
selectedPaymentMethod !== EPaymentMethod.PAYMENT_BUTTONS
}
onAvailable={(_isAvailable, _availableMethods) =>
onAvailableExpressCheckout(_isAvailable, _availableMethods)
}
onChangeLoading={(isLoading) =>
setIsLoadingExpressCheckout(isLoading)
}
/>
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
<CheckoutForm
confirmType={paymentType}
subscriptionReceiptId={paymentIntentId}
returnUrl={returnUrl}
/>
)}
</Elements>
)}
</div>
<SecurityPayments />
<p className={styles.address}>500 N RAINBOW BLVD LAS VEGAS, NV 89107</p>
</div>
<SecurityPayments />
<p className={styles.address}>500 N RAINBOW BLVD LAS VEGAS, NV 89107</p>
</div>
</>
);
}

View File

@ -8,6 +8,12 @@
color: #2f2e37;
}
.payment-modal.hide {
min-height: 0;
height: 0;
opacity: 0;
}
.title {
font-weight: 700;
font-size: 20px;

View File

@ -4,26 +4,28 @@ import { TCanMakePaymentResult } from "@/components/PaymentPage/methods/StripeBu
interface IPaymentButtonsProps {
availableMethods: TCanMakePaymentResult;
}
const Image = ({ availableMethods }: IPaymentButtonsProps) => {
if (!availableMethods) return <></>;
if (availableMethods["applePay"]) {
return <img src="/applepay.webp" alt="ApplePay" />;
}
if (availableMethods["googlePay"]) {
return <img src="/google-pay-mark.png" alt="google" />;
}
if (availableMethods["amazon"]) {
return <img src="/amazon-pay-mark.png" alt="AmazonPay" />;
}
if (availableMethods["link"]) {
return <img src="/link-pay-mark.png" alt="LinkPay" />;
}
return <></>;
};
function PaymentButtons({ availableMethods }: IPaymentButtonsProps) {
if (!availableMethods) return <></>;
return (
<div className={styles.container}>
{availableMethods["applePay"] && (
<img src="/applepay.webp" alt="ApplePay" />
)}
{availableMethods["googlePay"] && (
<img
src="/google-pay-mark.png"
alt="google"
style={{
height: "36px",
}}
/>
)}
{availableMethods["link"] && (
<img src="/link-pay-mark.png" alt="LinkPay" />
)}
<Image availableMethods={availableMethods} />
</div>
);
}