AW-104-stripe-button
This commit is contained in:
parent
cacd28b395
commit
5fdaa06f85
BIN
public/google-pay-mark.png
Normal file
BIN
public/google-pay-mark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
public/link-pay-mark.png
Normal file
BIN
public/link-pay-mark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@ -1,121 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import {
|
|
||||||
PaymentRequestButtonElement,
|
|
||||||
useStripe,
|
|
||||||
useElements,
|
|
||||||
} from "@stripe/react-stripe-js";
|
|
||||||
import { PaymentRequest } from "@stripe/stripe-js";
|
|
||||||
import styles from "./styles.module.css";
|
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import routes from "@/routes";
|
|
||||||
import { IPaywallProduct } from "@/api/resources/Paywall";
|
|
||||||
|
|
||||||
interface ApplePayButtonProps {
|
|
||||||
activeProduct: IPaywallProduct | null;
|
|
||||||
client_secret: string;
|
|
||||||
subscriptionReceiptId?: string;
|
|
||||||
returnUrl?: string;
|
|
||||||
setCanMakePayment?: (isCanMakePayment: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ApplePayButton({
|
|
||||||
activeProduct,
|
|
||||||
client_secret,
|
|
||||||
subscriptionReceiptId,
|
|
||||||
returnUrl,
|
|
||||||
setCanMakePayment,
|
|
||||||
}: ApplePayButtonProps) {
|
|
||||||
const stripe = useStripe();
|
|
||||||
const elements = useElements();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const getAmountFromProduct = (subPlan: IPaywallProduct) => {
|
|
||||||
if (subPlan.isTrial) {
|
|
||||||
return subPlan.trialPrice;
|
|
||||||
}
|
|
||||||
return subPlan.price;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!stripe || !elements || !activeProduct) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pr = stripe.paymentRequest({
|
|
||||||
country: "US",
|
|
||||||
currency: "usd",
|
|
||||||
total: {
|
|
||||||
label: activeProduct.name || "Subscription",
|
|
||||||
amount: getAmountFromProduct(activeProduct),
|
|
||||||
},
|
|
||||||
requestPayerName: true,
|
|
||||||
requestPayerEmail: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
pr.canMakePayment().then((result) => {
|
|
||||||
if (result) {
|
|
||||||
setPaymentRequest(pr);
|
|
||||||
setCanMakePayment?.(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
pr.on("paymentmethod", async (e) => {
|
|
||||||
const { error: stripeError, paymentIntent } =
|
|
||||||
await stripe.confirmCardPayment(
|
|
||||||
client_secret,
|
|
||||||
{
|
|
||||||
payment_method: e.paymentMethod.id,
|
|
||||||
},
|
|
||||||
{ handleActions: false }
|
|
||||||
);
|
|
||||||
paymentIntent;
|
|
||||||
|
|
||||||
if (stripeError) {
|
|
||||||
// Show error to your customer (e.g., insufficient funds)
|
|
||||||
navigate(
|
|
||||||
`${routes.client.paymentResult()}/${subscriptionReceiptId}/?redirect_status=failed`
|
|
||||||
);
|
|
||||||
return e.complete("fail");
|
|
||||||
}
|
|
||||||
navigate(
|
|
||||||
returnUrl ||
|
|
||||||
`${routes.client.paymentResult()}/${subscriptionReceiptId}/?redirect_status=succeeded`
|
|
||||||
);
|
|
||||||
e.complete("success");
|
|
||||||
// Show a success message to your customer
|
|
||||||
// There's a risk of the customer closing the window before callback
|
|
||||||
// execution. Set up a webhook or plugin to listen for the
|
|
||||||
// payment_intent.succeeded event that handles any business critical
|
|
||||||
// post-payment actions.
|
|
||||||
});
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [
|
|
||||||
client_secret,
|
|
||||||
dispatch,
|
|
||||||
elements,
|
|
||||||
navigate,
|
|
||||||
stripe,
|
|
||||||
subscriptionReceiptId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{paymentRequest && (
|
|
||||||
<PaymentRequestButtonElement
|
|
||||||
className={styles["stripe-element"]}
|
|
||||||
options={{
|
|
||||||
paymentRequest,
|
|
||||||
style: { paymentRequestButton: { height: "60px" } },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ApplePayButton;
|
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { IPaywallProduct } from "@/api/resources/Paywall";
|
||||||
|
import { useCanUseStripeButton } from "@/hooks/payment/useCanUseStripeButton";
|
||||||
|
import { actions } from "@/store";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
|
||||||
|
interface ICheckAvailableStripeButtonProps {
|
||||||
|
activeProduct: IPaywallProduct | null;
|
||||||
|
clientSecret: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CheckAvailableStripeButton({
|
||||||
|
activeProduct,
|
||||||
|
clientSecret,
|
||||||
|
}: ICheckAvailableStripeButtonProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { paymentRequest, availableMethods } = useCanUseStripeButton({
|
||||||
|
activeProduct,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (paymentRequest && availableMethods) {
|
||||||
|
dispatch(
|
||||||
|
actions.payment.updateStripeButton({ paymentRequest, availableMethods })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [availableMethods, dispatch, paymentRequest]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CheckAvailableStripeButton;
|
||||||
27
src/components/PaymentPage/methods/StripeButton/index.tsx
Normal file
27
src/components/PaymentPage/methods/StripeButton/index.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { PaymentRequestButtonElement } from "@stripe/react-stripe-js";
|
||||||
|
import { CanMakePaymentResult, PaymentRequest } from "@stripe/stripe-js";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
export type TCanMakePaymentResult = CanMakePaymentResult | null;
|
||||||
|
|
||||||
|
interface ApplePayButtonProps {
|
||||||
|
paymentRequest: PaymentRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
function StripeButton({ paymentRequest }: ApplePayButtonProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{paymentRequest && (
|
||||||
|
<PaymentRequestButtonElement
|
||||||
|
className={styles["stripe-element"]}
|
||||||
|
options={{
|
||||||
|
paymentRequest,
|
||||||
|
style: { paymentRequestButton: { height: "60px" } },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StripeButton;
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import styles from "./styles.module.css";
|
|
||||||
import Title from "@/components/Title";
|
|
||||||
|
|
||||||
interface ITotalTodayProps {
|
|
||||||
total: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TotalToday({ total }: ITotalTodayProps): JSX.Element {
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<Title className={styles.text} variant="h3">{"Total today:"}</Title>
|
|
||||||
<Title className={styles.text} variant="h3">{total}</Title>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TotalToday;
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
background-color: #e7f5ee;
|
|
||||||
border-radius: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #000;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import styles from "./styles.module.css";
|
|
||||||
import TotalToday from "./TotalToday";
|
|
||||||
import ApplePayButton from "../PaymentPage/methods/ApplePayButton";
|
|
||||||
import { IPaywallProduct } from "@/api/resources/Paywall";
|
|
||||||
|
|
||||||
interface ISubPlanInformationProps {
|
|
||||||
product: IPaywallProduct;
|
|
||||||
client_secret?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPrice = (product: IPaywallProduct): string => {
|
|
||||||
return `$${
|
|
||||||
(product.trialPrice === 100 ? 99 : product.trialPrice || 0) / 100
|
|
||||||
}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
function SubPlanInformation({
|
|
||||||
product,
|
|
||||||
client_secret,
|
|
||||||
}: ISubPlanInformationProps): JSX.Element {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<TotalToday total={getPrice(product)} />
|
|
||||||
{client_secret && (
|
|
||||||
<ApplePayButton activeProduct={product} client_secret={client_secret} />
|
|
||||||
)}
|
|
||||||
<p className={styles.description}>
|
|
||||||
{t("auweb.pay.information").replaceAll("%@", getPrice(product))}.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SubPlanInformation;
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 300px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #666666;
|
|
||||||
text-align: left;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 16px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-pal-button {
|
|
||||||
width: 100%;
|
|
||||||
height: 60px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: #ffc43a;
|
|
||||||
border-radius: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.errors {
|
|
||||||
color: red;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
@ -4,7 +4,6 @@ import PaymentMethodsChoice from "../PaymentMethodsChoice";
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods";
|
import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods";
|
||||||
import { Elements } from "@stripe/react-stripe-js";
|
import { Elements } from "@stripe/react-stripe-js";
|
||||||
import ApplePayButton from "@/components/PaymentPage/methods/ApplePayButton";
|
|
||||||
import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm";
|
import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm";
|
||||||
import { Stripe, loadStripe } from "@stripe/stripe-js";
|
import { Stripe, loadStripe } from "@stripe/stripe-js";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
@ -16,6 +15,8 @@ import SecurityPayments from "../SecurityPayments";
|
|||||||
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
|
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
|
||||||
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
||||||
import { useMakePayment } from "@/hooks/payment/useMakePayment";
|
import { useMakePayment } from "@/hooks/payment/useMakePayment";
|
||||||
|
import StripeButton from "@/components/PaymentPage/methods/StripeButton";
|
||||||
|
import CheckAvailableStripeButton from "@/components/PaymentPage/methods/StripeButton/CheckAvailableStripeButton";
|
||||||
|
|
||||||
interface IPaymentModalProps {
|
interface IPaymentModalProps {
|
||||||
activeProduct?: IPaywallProduct;
|
activeProduct?: IPaywallProduct;
|
||||||
@ -41,7 +42,7 @@ function PaymentModal({
|
|||||||
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
|
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
|
||||||
const _activeProduct = activeProduct ? activeProduct : activeProductFromStore;
|
const _activeProduct = activeProduct ? activeProduct : activeProductFromStore;
|
||||||
const { products, paywallId, placementId } = usePaywall({ placementKey });
|
const { products, paywallId, placementId } = usePaywall({ placementKey });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
paymentIntentId,
|
paymentIntentId,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
@ -57,16 +58,21 @@ function PaymentModal({
|
|||||||
returnPaidUrl: returnUrl,
|
returnPaidUrl: returnUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const stripeButton = useSelector(selectors.selectStripeButton);
|
||||||
|
|
||||||
|
const paymentRequest = stripeButton?.paymentRequest;
|
||||||
|
const availableMethods = stripeButton?.availableMethods;
|
||||||
|
|
||||||
if (checkoutUrl?.length) {
|
if (checkoutUrl?.length) {
|
||||||
window.location.href = checkoutUrl;
|
window.location.href = checkoutUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentMethodsButtons = useMemo(() => {
|
const paymentMethodsButtons = useMemo(() => {
|
||||||
return paymentMethods;
|
return paymentMethods(availableMethods);
|
||||||
}, []);
|
}, [availableMethods]);
|
||||||
|
|
||||||
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
|
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
|
||||||
EPaymentMethod.PAYMENT_BUTTONS
|
paymentMethodsButtons[0].id
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSelectPaymentMethod = (method: EPaymentMethod) => {
|
const onSelectPaymentMethod = (method: EPaymentMethod) => {
|
||||||
@ -138,13 +144,15 @@ function PaymentModal({
|
|||||||
<div className={styles["payment-method-container"]}>
|
<div className={styles["payment-method-container"]}>
|
||||||
{stripePromise && clientSecret && (
|
{stripePromise && clientSecret && (
|
||||||
<Elements stripe={stripePromise} options={{ clientSecret }}>
|
<Elements stripe={stripePromise} options={{ clientSecret }}>
|
||||||
|
<CheckAvailableStripeButton
|
||||||
|
activeProduct={_activeProduct}
|
||||||
|
clientSecret={clientSecret}
|
||||||
|
/>
|
||||||
{selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && (
|
{selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && (
|
||||||
<div className={styles["payment-method"]}>
|
<div className={styles["payment-method"]}>
|
||||||
<ApplePayButton
|
{paymentRequest && (
|
||||||
activeProduct={_activeProduct}
|
<StripeButton paymentRequest={paymentRequest} />
|
||||||
client_secret={clientSecret}
|
)}
|
||||||
subscriptionReceiptId={paymentIntentId}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import PaymentMethodsChoice from "../PaymentMethodsChoice";
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods";
|
import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods";
|
||||||
import { Elements } from "@stripe/react-stripe-js";
|
import { Elements } from "@stripe/react-stripe-js";
|
||||||
import ApplePayButton from "@/components/PaymentPage/methods/ApplePayButton";
|
|
||||||
import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm";
|
import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm";
|
||||||
import { Stripe, loadStripe } from "@stripe/stripe-js";
|
import { Stripe, loadStripe } from "@stripe/stripe-js";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
@ -14,6 +13,8 @@ import SecurityPayments from "../SecurityPayments";
|
|||||||
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
|
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
|
||||||
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
||||||
import { useMakePayment } from "@/hooks/payment/useMakePayment";
|
import { useMakePayment } from "@/hooks/payment/useMakePayment";
|
||||||
|
import StripeButton from "@/components/PaymentPage/methods/StripeButton";
|
||||||
|
import CheckAvailableStripeButton from "@/components/PaymentPage/methods/StripeButton/CheckAvailableStripeButton";
|
||||||
|
|
||||||
interface IPaymentModalProps {
|
interface IPaymentModalProps {
|
||||||
activeProduct?: IPaywallProduct;
|
activeProduct?: IPaywallProduct;
|
||||||
@ -59,16 +60,17 @@ function PaymentModal({
|
|||||||
returnPaidUrl: returnUrl,
|
returnPaidUrl: returnUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { paymentRequest, availableMethods } = useSelector(
|
||||||
|
selectors.selectStripeButton
|
||||||
|
);
|
||||||
|
|
||||||
if (checkoutUrl?.length) {
|
if (checkoutUrl?.length) {
|
||||||
window.location.href = checkoutUrl;
|
window.location.href = checkoutUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentMethodsButtons = useMemo(() => {
|
const paymentMethodsButtons = useMemo(() => {
|
||||||
// return paymentMethods.filter(
|
return paymentMethods(availableMethods);
|
||||||
// (method) => method.id !== EPaymentMethod.PAYMENT_BUTTONS
|
}, [availableMethods]);
|
||||||
// );
|
|
||||||
return paymentMethods;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
|
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
|
||||||
EPaymentMethod.PAYMENT_BUTTONS
|
EPaymentMethod.PAYMENT_BUTTONS
|
||||||
@ -137,13 +139,15 @@ function PaymentModal({
|
|||||||
<div className={styles["payment-method-container"]}>
|
<div className={styles["payment-method-container"]}>
|
||||||
{stripePromise && clientSecret && (
|
{stripePromise && clientSecret && (
|
||||||
<Elements stripe={stripePromise} options={{ clientSecret }}>
|
<Elements stripe={stripePromise} options={{ clientSecret }}>
|
||||||
|
<CheckAvailableStripeButton
|
||||||
|
activeProduct={_activeProduct}
|
||||||
|
clientSecret={clientSecret}
|
||||||
|
/>
|
||||||
{selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && (
|
{selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && (
|
||||||
<div className={styles["payment-method"]}>
|
<div className={styles["payment-method"]}>
|
||||||
<ApplePayButton
|
{paymentRequest && (
|
||||||
activeProduct={_activeProduct}
|
<StripeButton paymentRequest={paymentRequest} />
|
||||||
client_secret={clientSecret}
|
)}
|
||||||
subscriptionReceiptId={paymentIntentId}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,31 @@
|
|||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
|
import { TCanMakePaymentResult } from "@/components/PaymentPage/methods/StripeButton";
|
||||||
|
|
||||||
function PaymentButtons() {
|
interface IPaymentButtonsProps {
|
||||||
return <div className={styles.container}>
|
availableMethods: TCanMakePaymentResult;
|
||||||
<img src="/applepay.webp" alt="ApplePay" />
|
}
|
||||||
</div>;
|
|
||||||
|
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" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PaymentButtons;
|
export default PaymentButtons;
|
||||||
|
|||||||
@ -8,5 +8,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container > img {
|
.container > img {
|
||||||
height: 16px;
|
height: 22px;
|
||||||
}
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { TCanMakePaymentResult } from "@/components/PaymentPage/methods/StripeButton";
|
||||||
import CreditCard from "@/components/ui/PaymentMethodsButtons/CreditCard";
|
import CreditCard from "@/components/ui/PaymentMethodsButtons/CreditCard";
|
||||||
import PaymentButtons from "@/components/ui/PaymentMethodsButtons/PaymentButtons";
|
import PaymentButtons from "@/components/ui/PaymentMethodsButtons/PaymentButtons";
|
||||||
|
|
||||||
@ -11,13 +12,35 @@ export interface IPaymentMethod {
|
|||||||
component: JSX.Element;
|
component: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const paymentMethods: IPaymentMethod[] = [
|
// export const paymentMethods: IPaymentMethod[] = [
|
||||||
{
|
// {
|
||||||
id: EPaymentMethod.PAYMENT_BUTTONS,
|
// id: EPaymentMethod.PAYMENT_BUTTONS,
|
||||||
component: <PaymentButtons />,
|
// component: <PaymentButtons />,
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: EPaymentMethod.CREDIT_CARD,
|
// id: EPaymentMethod.CREDIT_CARD,
|
||||||
component: <CreditCard />,
|
// component: <CreditCard />,
|
||||||
},
|
// },
|
||||||
];
|
// ];
|
||||||
|
|
||||||
|
export function paymentMethods(
|
||||||
|
availableMethods: TCanMakePaymentResult
|
||||||
|
): IPaymentMethod[] {
|
||||||
|
let methods = [
|
||||||
|
{
|
||||||
|
id: EPaymentMethod.PAYMENT_BUTTONS,
|
||||||
|
component: <PaymentButtons availableMethods={availableMethods} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: EPaymentMethod.CREDIT_CARD,
|
||||||
|
component: <CreditCard />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!availableMethods) {
|
||||||
|
methods = methods.filter(
|
||||||
|
(method) => method.id !== EPaymentMethod.PAYMENT_BUTTONS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return methods;
|
||||||
|
}
|
||||||
|
|||||||
98
src/hooks/payment/useCanUseStripeButton.ts
Normal file
98
src/hooks/payment/useCanUseStripeButton.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { IPaywallProduct } from "@/api/resources/Paywall";
|
||||||
|
import routes from "@/routes";
|
||||||
|
import { useElements, useStripe } from "@stripe/react-stripe-js";
|
||||||
|
import { CanMakePaymentResult, PaymentRequest } from "@stripe/stripe-js";
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export type TCanMakePaymentResult = CanMakePaymentResult | null;
|
||||||
|
|
||||||
|
|
||||||
|
const getAmountFromProduct = (subPlan: IPaywallProduct) => {
|
||||||
|
if (subPlan.isTrial) {
|
||||||
|
return subPlan.trialPrice;
|
||||||
|
}
|
||||||
|
return subPlan.price;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IUseCanUseStripeButton {
|
||||||
|
activeProduct: IPaywallProduct | null;
|
||||||
|
client_secret: string;
|
||||||
|
subscriptionReceiptId?: string;
|
||||||
|
returnUrl?: string;
|
||||||
|
setCanMakePayment?: (canMakePayment: TCanMakePaymentResult) => void;
|
||||||
|
setCanMakePaymentLoading?: (loading: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCanUseStripeButton = ({
|
||||||
|
activeProduct,
|
||||||
|
client_secret,
|
||||||
|
subscriptionReceiptId,
|
||||||
|
returnUrl,
|
||||||
|
}: IUseCanUseStripeButton) => {
|
||||||
|
const stripe = useStripe();
|
||||||
|
const elements = useElements();
|
||||||
|
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [availableMethods, setAvailableMethods] = useState<TCanMakePaymentResult>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!stripe || !elements || !activeProduct) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pr = stripe.paymentRequest({
|
||||||
|
country: "US",
|
||||||
|
currency: "usd",
|
||||||
|
total: {
|
||||||
|
label: activeProduct.name || "Subscription",
|
||||||
|
amount: getAmountFromProduct(activeProduct),
|
||||||
|
},
|
||||||
|
requestPayerName: true,
|
||||||
|
requestPayerEmail: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
pr.canMakePayment()
|
||||||
|
.then((result) => {
|
||||||
|
if (result) {
|
||||||
|
setPaymentRequest(pr)
|
||||||
|
setAvailableMethods(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
pr.on("paymentmethod", async (e) => {
|
||||||
|
const { error: stripeError, paymentIntent } =
|
||||||
|
await stripe.confirmCardPayment(
|
||||||
|
client_secret,
|
||||||
|
{
|
||||||
|
payment_method: e.paymentMethod.id,
|
||||||
|
},
|
||||||
|
{ handleActions: false }
|
||||||
|
);
|
||||||
|
paymentIntent;
|
||||||
|
|
||||||
|
if (stripeError) {
|
||||||
|
// Show error to your customer (e.g., insufficient funds)
|
||||||
|
navigate(
|
||||||
|
`${routes.client.paymentResult()}/${subscriptionReceiptId}/?redirect_status=failed`
|
||||||
|
);
|
||||||
|
return e.complete("fail");
|
||||||
|
}
|
||||||
|
navigate(
|
||||||
|
returnUrl ||
|
||||||
|
`${routes.client.paymentResult()}/${subscriptionReceiptId}/?redirect_status=succeeded`
|
||||||
|
);
|
||||||
|
e.complete("success");
|
||||||
|
});
|
||||||
|
}, [activeProduct, client_secret, elements, navigate, returnUrl, stripe, subscriptionReceiptId]);
|
||||||
|
|
||||||
|
return useMemo(() => ({
|
||||||
|
paymentRequest,
|
||||||
|
availableMethods
|
||||||
|
}), [
|
||||||
|
paymentRequest,
|
||||||
|
availableMethods
|
||||||
|
])
|
||||||
|
}
|
||||||
@ -32,6 +32,7 @@ import payment, {
|
|||||||
actions as paymentActions,
|
actions as paymentActions,
|
||||||
selectActiveProduct,
|
selectActiveProduct,
|
||||||
selectIsDiscount,
|
selectIsDiscount,
|
||||||
|
selectStripeButton,
|
||||||
selectSubscriptionReceipt,
|
selectSubscriptionReceipt,
|
||||||
} from "./payment";
|
} from "./payment";
|
||||||
import subscriptionPlans, {
|
import subscriptionPlans, {
|
||||||
@ -123,6 +124,7 @@ export const selectors = {
|
|||||||
selectPaywalls,
|
selectPaywalls,
|
||||||
selectPaywallsIsMustUpdate,
|
selectPaywallsIsMustUpdate,
|
||||||
selectPrivacyPolicy,
|
selectPrivacyPolicy,
|
||||||
|
selectStripeButton,
|
||||||
...formSelectors,
|
...formSelectors,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,21 @@
|
|||||||
import { IPaywallProduct } from "@/api/resources/Paywall";
|
import { IPaywallProduct } from "@/api/resources/Paywall";
|
||||||
import { SubscriptionReceipt } from "@/api/resources/UserSubscriptionReceipts";
|
import { SubscriptionReceipt } from "@/api/resources/UserSubscriptionReceipts";
|
||||||
|
import { TCanMakePaymentResult } from "@/hooks/payment/useCanUseStripeButton";
|
||||||
import { createSlice, createSelector } from "@reduxjs/toolkit";
|
import { createSlice, createSelector } from "@reduxjs/toolkit";
|
||||||
import type { PayloadAction } from "@reduxjs/toolkit";
|
import type { PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
import { PaymentRequest } from "@stripe/stripe-js";
|
||||||
|
|
||||||
|
interface IStripeButton {
|
||||||
|
paymentRequest: PaymentRequest | null;
|
||||||
|
availableMethods: TCanMakePaymentResult;
|
||||||
|
}
|
||||||
|
|
||||||
interface IPayment {
|
interface IPayment {
|
||||||
selectedPrice: number | null;
|
selectedPrice: number | null;
|
||||||
isDiscount: boolean;
|
isDiscount: boolean;
|
||||||
subscriptionReceipt: SubscriptionReceipt | null;
|
subscriptionReceipt: SubscriptionReceipt | null;
|
||||||
activeProduct: IPaywallProduct | null;
|
activeProduct: IPaywallProduct | null;
|
||||||
|
stripeButton: IStripeButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: IPayment = {
|
const initialState: IPayment = {
|
||||||
@ -15,6 +23,10 @@ const initialState: IPayment = {
|
|||||||
isDiscount: false,
|
isDiscount: false,
|
||||||
subscriptionReceipt: null,
|
subscriptionReceipt: null,
|
||||||
activeProduct: null,
|
activeProduct: null,
|
||||||
|
stripeButton: {
|
||||||
|
paymentRequest: null,
|
||||||
|
availableMethods: null,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const paymentSlice = createSlice({
|
const paymentSlice = createSlice({
|
||||||
@ -24,6 +36,9 @@ const paymentSlice = createSlice({
|
|||||||
update(state, action: PayloadAction<Partial<IPayment>>) {
|
update(state, action: PayloadAction<Partial<IPayment>>) {
|
||||||
return { ...state, ...action.payload };
|
return { ...state, ...action.payload };
|
||||||
},
|
},
|
||||||
|
updateStripeButton(state, action: PayloadAction<IStripeButton>) {
|
||||||
|
return { ...state, stripeButton: action.payload };
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => builder.addCase("reset", () => initialState),
|
extraReducers: (builder) => builder.addCase("reset", () => initialState),
|
||||||
});
|
});
|
||||||
@ -45,4 +60,8 @@ export const selectSubscriptionReceipt = createSelector(
|
|||||||
(state: { payment: IPayment }) => state.payment.subscriptionReceipt,
|
(state: { payment: IPayment }) => state.payment.subscriptionReceipt,
|
||||||
(payment) => payment
|
(payment) => payment
|
||||||
);
|
);
|
||||||
|
export const selectStripeButton = createSelector(
|
||||||
|
(state: { payment: IPayment }) => state.payment.stripeButton,
|
||||||
|
(payment) => payment
|
||||||
|
);
|
||||||
export default paymentSlice.reducer;
|
export default paymentSlice.reducer;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user