diff --git a/public/google-pay-mark.png b/public/google-pay-mark.png new file mode 100644 index 0000000..07cd76a Binary files /dev/null and b/public/google-pay-mark.png differ diff --git a/public/link-pay-mark.png b/public/link-pay-mark.png new file mode 100644 index 0000000..4184628 Binary files /dev/null and b/public/link-pay-mark.png differ diff --git a/src/components/PaymentPage/methods/ApplePayButton/index.tsx b/src/components/PaymentPage/methods/ApplePayButton/index.tsx deleted file mode 100644 index 6e88090..0000000 --- a/src/components/PaymentPage/methods/ApplePayButton/index.tsx +++ /dev/null @@ -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( - 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 && ( - - )} - - ); -} - -export default ApplePayButton; diff --git a/src/components/PaymentPage/methods/StripeButton/CheckAvailableStripeButton/index.tsx b/src/components/PaymentPage/methods/StripeButton/CheckAvailableStripeButton/index.tsx new file mode 100644 index 0000000..3b686d5 --- /dev/null +++ b/src/components/PaymentPage/methods/StripeButton/CheckAvailableStripeButton/index.tsx @@ -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; diff --git a/src/components/PaymentPage/methods/StripeButton/index.tsx b/src/components/PaymentPage/methods/StripeButton/index.tsx new file mode 100644 index 0000000..9b47aa1 --- /dev/null +++ b/src/components/PaymentPage/methods/StripeButton/index.tsx @@ -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 && ( + + )} + + ); +} + +export default StripeButton; diff --git a/src/components/PaymentPage/methods/ApplePayButton/styles.module.css b/src/components/PaymentPage/methods/StripeButton/styles.module.css similarity index 100% rename from src/components/PaymentPage/methods/ApplePayButton/styles.module.css rename to src/components/PaymentPage/methods/StripeButton/styles.module.css diff --git a/src/components/SubPlanInformation/TotalToday/index.tsx b/src/components/SubPlanInformation/TotalToday/index.tsx deleted file mode 100644 index d36b3c4..0000000 --- a/src/components/SubPlanInformation/TotalToday/index.tsx +++ /dev/null @@ -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 ( -
- {"Total today:"} - {total} -
- ); -} - -export default TotalToday; diff --git a/src/components/SubPlanInformation/TotalToday/styles.module.css b/src/components/SubPlanInformation/TotalToday/styles.module.css deleted file mode 100644 index fe619d9..0000000 --- a/src/components/SubPlanInformation/TotalToday/styles.module.css +++ /dev/null @@ -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; -} diff --git a/src/components/SubPlanInformation/index.tsx b/src/components/SubPlanInformation/index.tsx deleted file mode 100644 index c872f5c..0000000 --- a/src/components/SubPlanInformation/index.tsx +++ /dev/null @@ -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 ( -
- - {client_secret && ( - - )} -

- {t("auweb.pay.information").replaceAll("%@", getPrice(product))}. -

-
- ); -} - -export default SubPlanInformation; diff --git a/src/components/SubPlanInformation/styles.module.css b/src/components/SubPlanInformation/styles.module.css deleted file mode 100644 index 7c6cd7d..0000000 --- a/src/components/SubPlanInformation/styles.module.css +++ /dev/null @@ -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; -} diff --git a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PaymentModal/index.tsx b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PaymentModal/index.tsx index 5f14274..4b4412d 100644 --- a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PaymentModal/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PaymentModal/index.tsx @@ -4,7 +4,6 @@ import PaymentMethodsChoice from "../PaymentMethodsChoice"; import { useEffect, useMemo, useState } from "react"; import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods"; import { Elements } from "@stripe/react-stripe-js"; -import ApplePayButton from "@/components/PaymentPage/methods/ApplePayButton"; import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm"; import { Stripe, loadStripe } from "@stripe/stripe-js"; import { useSelector } from "react-redux"; @@ -16,6 +15,8 @@ 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"; interface IPaymentModalProps { activeProduct?: IPaywallProduct; @@ -41,7 +42,7 @@ function PaymentModal({ const activeProductFromStore = useSelector(selectors.selectActiveProduct); const _activeProduct = activeProduct ? activeProduct : activeProductFromStore; const { products, paywallId, placementId } = usePaywall({ placementKey }); - + const { paymentIntentId, clientSecret, @@ -57,16 +58,21 @@ function PaymentModal({ returnPaidUrl: returnUrl, }); + const stripeButton = useSelector(selectors.selectStripeButton); + + const paymentRequest = stripeButton?.paymentRequest; + const availableMethods = stripeButton?.availableMethods; + if (checkoutUrl?.length) { window.location.href = checkoutUrl; } const paymentMethodsButtons = useMemo(() => { - return paymentMethods; - }, []); + return paymentMethods(availableMethods); + }, [availableMethods]); const [selectedPaymentMethod, setSelectedPaymentMethod] = useState( - EPaymentMethod.PAYMENT_BUTTONS + paymentMethodsButtons[0].id ); const onSelectPaymentMethod = (method: EPaymentMethod) => { @@ -138,13 +144,15 @@ function PaymentModal({
{stripePromise && clientSecret && ( + {selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && (
- + {paymentRequest && ( + + )}
)} diff --git a/src/components/pages/TrialPayment/components/PaymentModal/index.tsx b/src/components/pages/TrialPayment/components/PaymentModal/index.tsx index a6f9908..8f11bdd 100644 --- a/src/components/pages/TrialPayment/components/PaymentModal/index.tsx +++ b/src/components/pages/TrialPayment/components/PaymentModal/index.tsx @@ -4,7 +4,6 @@ import PaymentMethodsChoice from "../PaymentMethodsChoice"; import { useEffect, useMemo, useState } from "react"; import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods"; import { Elements } from "@stripe/react-stripe-js"; -import ApplePayButton from "@/components/PaymentPage/methods/ApplePayButton"; import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm"; import { Stripe, loadStripe } from "@stripe/stripe-js"; import { useSelector } from "react-redux"; @@ -14,6 +13,8 @@ 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"; interface IPaymentModalProps { activeProduct?: IPaywallProduct; @@ -59,16 +60,17 @@ function PaymentModal({ returnPaidUrl: returnUrl, }); + const { paymentRequest, availableMethods } = useSelector( + selectors.selectStripeButton + ); + if (checkoutUrl?.length) { window.location.href = checkoutUrl; } const paymentMethodsButtons = useMemo(() => { - // return paymentMethods.filter( - // (method) => method.id !== EPaymentMethod.PAYMENT_BUTTONS - // ); - return paymentMethods; - }, []); + return paymentMethods(availableMethods); + }, [availableMethods]); const [selectedPaymentMethod, setSelectedPaymentMethod] = useState( EPaymentMethod.PAYMENT_BUTTONS @@ -137,13 +139,15 @@ function PaymentModal({
{stripePromise && clientSecret && ( + {selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && (
- + {paymentRequest && ( + + )}
)} diff --git a/src/components/ui/PaymentMethodsButtons/PaymentButtons/index.tsx b/src/components/ui/PaymentMethodsButtons/PaymentButtons/index.tsx index dd1721a..1faf69c 100644 --- a/src/components/ui/PaymentMethodsButtons/PaymentButtons/index.tsx +++ b/src/components/ui/PaymentMethodsButtons/PaymentButtons/index.tsx @@ -1,9 +1,31 @@ import styles from "./styles.module.css"; +import { TCanMakePaymentResult } from "@/components/PaymentPage/methods/StripeButton"; -function PaymentButtons() { - return
- ApplePay -
; +interface IPaymentButtonsProps { + availableMethods: TCanMakePaymentResult; +} + +function PaymentButtons({ availableMethods }: IPaymentButtonsProps) { + if (!availableMethods) return <>; + return ( +
+ {availableMethods["applePay"] && ( + ApplePay + )} + {availableMethods["googlePay"] && ( + google + )} + {availableMethods["link"] && ( + LinkPay + )} +
+ ); } export default PaymentButtons; diff --git a/src/components/ui/PaymentMethodsButtons/PaymentButtons/styles.module.css b/src/components/ui/PaymentMethodsButtons/PaymentButtons/styles.module.css index 7d1360f..0bad093 100644 --- a/src/components/ui/PaymentMethodsButtons/PaymentButtons/styles.module.css +++ b/src/components/ui/PaymentMethodsButtons/PaymentButtons/styles.module.css @@ -8,5 +8,5 @@ } .container > img { - height: 16px; + height: 22px; } \ No newline at end of file diff --git a/src/data/paymentMethods.tsx b/src/data/paymentMethods.tsx index 5428dc2..f827a26 100644 --- a/src/data/paymentMethods.tsx +++ b/src/data/paymentMethods.tsx @@ -1,3 +1,4 @@ +import { TCanMakePaymentResult } from "@/components/PaymentPage/methods/StripeButton"; import CreditCard from "@/components/ui/PaymentMethodsButtons/CreditCard"; import PaymentButtons from "@/components/ui/PaymentMethodsButtons/PaymentButtons"; @@ -11,13 +12,35 @@ export interface IPaymentMethod { component: JSX.Element; } -export const paymentMethods: IPaymentMethod[] = [ - { - id: EPaymentMethod.PAYMENT_BUTTONS, - component: , - }, - { - id: EPaymentMethod.CREDIT_CARD, - component: , - }, -]; +// export const paymentMethods: IPaymentMethod[] = [ +// { +// id: EPaymentMethod.PAYMENT_BUTTONS, +// component: , +// }, +// { +// id: EPaymentMethod.CREDIT_CARD, +// component: , +// }, +// ]; + +export function paymentMethods( + availableMethods: TCanMakePaymentResult +): IPaymentMethod[] { + let methods = [ + { + id: EPaymentMethod.PAYMENT_BUTTONS, + component: , + }, + { + id: EPaymentMethod.CREDIT_CARD, + component: , + }, + ]; + + if (!availableMethods) { + methods = methods.filter( + (method) => method.id !== EPaymentMethod.PAYMENT_BUTTONS + ); + } + return methods; +} diff --git a/src/hooks/payment/useCanUseStripeButton.ts b/src/hooks/payment/useCanUseStripeButton.ts new file mode 100644 index 0000000..f387d22 --- /dev/null +++ b/src/hooks/payment/useCanUseStripeButton.ts @@ -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( + null + ); + const [availableMethods, setAvailableMethods] = useState(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 + ]) +} \ No newline at end of file diff --git a/src/store/index.ts b/src/store/index.ts index c1357a4..33550ea 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -32,6 +32,7 @@ import payment, { actions as paymentActions, selectActiveProduct, selectIsDiscount, + selectStripeButton, selectSubscriptionReceipt, } from "./payment"; import subscriptionPlans, { @@ -123,6 +124,7 @@ export const selectors = { selectPaywalls, selectPaywallsIsMustUpdate, selectPrivacyPolicy, + selectStripeButton, ...formSelectors, }; diff --git a/src/store/payment.ts b/src/store/payment.ts index 23dfc9d..50d4d06 100644 --- a/src/store/payment.ts +++ b/src/store/payment.ts @@ -1,13 +1,21 @@ import { IPaywallProduct } from "@/api/resources/Paywall"; import { SubscriptionReceipt } from "@/api/resources/UserSubscriptionReceipts"; +import { TCanMakePaymentResult } from "@/hooks/payment/useCanUseStripeButton"; import { createSlice, createSelector } from "@reduxjs/toolkit"; import type { PayloadAction } from "@reduxjs/toolkit"; +import { PaymentRequest } from "@stripe/stripe-js"; + +interface IStripeButton { + paymentRequest: PaymentRequest | null; + availableMethods: TCanMakePaymentResult; +} interface IPayment { selectedPrice: number | null; isDiscount: boolean; subscriptionReceipt: SubscriptionReceipt | null; activeProduct: IPaywallProduct | null; + stripeButton: IStripeButton; } const initialState: IPayment = { @@ -15,6 +23,10 @@ const initialState: IPayment = { isDiscount: false, subscriptionReceipt: null, activeProduct: null, + stripeButton: { + paymentRequest: null, + availableMethods: null, + } }; const paymentSlice = createSlice({ @@ -24,6 +36,9 @@ const paymentSlice = createSlice({ update(state, action: PayloadAction>) { return { ...state, ...action.payload }; }, + updateStripeButton(state, action: PayloadAction) { + return { ...state, stripeButton: action.payload }; + }, }, extraReducers: (builder) => builder.addCase("reset", () => initialState), }); @@ -45,4 +60,8 @@ export const selectSubscriptionReceipt = createSelector( (state: { payment: IPayment }) => state.payment.subscriptionReceipt, (payment) => payment ); +export const selectStripeButton = createSelector( + (state: { payment: IPayment }) => state.payment.stripeButton, + (payment) => payment +); export default paymentSlice.reducer;