You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在现有React PayPal结账代码中集成月度定期支付功能?

如何在现有PayPal React代码中集成月度订阅支付功能

当然可以集成月度固定金额的捐款功能,PayPal的**订阅(Subscriptions)**功能正好适配这类定期扣款场景。下面是针对你现有代码的具体修改方案:

1. 添加订阅状态管理

首先在Redux的inputMoneyAmount reducer中新增状态字段,区分用户选择的是一次性支付还是月度订阅:

// reducer初始状态
const initialState = {
  amount: 0,
  flowAmount: 0,
  isMonthlySubscription: false // 新增订阅状态字段
};

// 新增对应的action
export const setIsMonthlySubscription = (isMonthly) => ({
  type: 'SET_IS_MONTHLY_SUBSCRIPTION',
  payload: isMonthly
});

// reducer处理逻辑
case 'SET_IS_MONTHLY_SUBSCRIPTION':
  return { ...state, isMonthlySubscription: action.payload };

2. 添加订阅切换UI

在组件的金额选择区域下方,添加切换按钮让用户选择是否开启月度捐款:

<div className="mt-6 mb-4">
  <button 
    onClick={() => dispatch(setIsMonthlySubscription(!isMonthly))}
    className={`px-6 py-2 rounded-lg font-medium ${
      isMonthly ? 'bg-blue-600 hover:bg-blue-700' : 'bg-gray-600 hover:bg-gray-700'
    }`}
  >
    {isMonthly ? '✅ 当前为月度自动捐款' : '🔄 切换为月度自动捐款'}
  </button>
</div>

同时在组件中获取该状态:

const isMonthly = useSelector(state => state.inputMoneyAmount.isMonthlySubscription);

3. 修改PayPal按钮逻辑,支持双模式切换

修改ButtonWrapper中的PayPalButtons组件,根据isMonthly状态切换一次性支付/订阅逻辑:

<PayPalButtons
  style={style}
  disabled={false}
  forceReRender={[amount, currency, style, isMonthly]}
  fundingSource={undefined}
  // 原有一次性支付逻辑
  createOrder={!isMonthly ? (data, actions) => {
    return actions.order
      .create({
        purchase_units: [
          {
            amount: {
              currency_code: currency,
              value: amount,
            },
          },
        ],
      })
      .then((orderId) => {
        return orderId;
      });
  } : undefined}
  // 新增订阅创建逻辑
  createSubscription={isMonthly ? (data, actions) => {
    return actions.subscription.create({
      // 推荐在PayPal后台预先创建订阅计划,直接填写plan_id
      plan_id: 'YOUR_PAYPAL_SUBSCRIPTION_PLAN_ID',
      // 若需前端动态生成计划(不推荐,建议后台创建更安全),可替换为以下配置:
      /*
      application_context: {
        brand_name: "Forward Ukraine Ministries",
        user_action: "SUBSCRIBE_NOW"
      },
      plan: {
        product_id: "YOUR_PAYPAL_PRODUCT_ID", // 需先在PayPal后台创建产品
        name: "月度捐款",
        description: "每月固定金额支持乌克兰人道主义救援",
        billing_cycles: [
          {
            frequency: { interval_unit: "MONTH", interval_count: 1 },
            tenure_type: "REGULAR",
            sequence: 1,
            total_cycles: 0, // 0表示无限期扣款,直到用户取消
            pricing_scheme: {
              fixed_price: { currency_code: currency, value: amount }
            }
          }
        ],
        payment_preferences: {
          auto_bill_outstanding: true,
          setup_fee: { currency_code: currency, value: "0" },
          setup_fee_failure_action: "CONTINUE",
          payment_failure_threshold: 3
        }
      }
      */
    }).then(subscriptionId => {
      // 将订阅ID同步到后端,用于后续管理
      console.log('订阅ID:', subscriptionId);
      return subscriptionId;
    });
  } : undefined}
  // 一次性支付确认逻辑(原有)
  onApprove={!isMonthly ? (data, actions) => {
    return actions.order.capture().then(function (details) {
      console.log('一次性支付成功:', details);
    });
  } : (data, actions) => {
    // 订阅确认逻辑(新增)
    return actions.subscription.get().then(details => {
      console.log('订阅创建成功:', details);
    });
  }}
/>

关键注意事项

  • PayPal后台配置:需在PayPal开发者平台(沙盒/生产环境)创建产品和订阅计划,获取对应的product_id和plan_id。
  • Webhook配置:必须配置PayPal Webhook接收订阅状态通知(如扣款成功、失败、用户取消订阅等),需后端配合处理。
  • 客户端ID替换:将代码中的"client-id": "test"替换为你在PayPal平台获取的真实客户端ID(测试用沙盒ID,生产用正式ID)。
  • 金额验证:确保月度捐款金额为合法数值,与一次性支付的金额验证逻辑保持一致。

修改后的完整组件代码

import React, { useEffect } from "react";
import {
  PayPalScriptProvider,
  PayPalButtons,
  usePayPalScriptReducer,
} from "@paypal/react-paypal-js";

import imgLogo from "../../../images/FUMlogo.png";
import imgColageDesk from "../../../images/Donate/colageDesk.png";
import DonateBTN from "../../UI/Button/DonateBTN/DonateBTN";
import DonateOther from "../../UI/Button/DonateBTN/DonateOther";
import { useDispatch, useSelector } from "react-redux";
import {
  setAmountMoney,
  setFlowAmountMoney,
  setIsMonthlySubscription
} from "../../../Store/Reducers/inputMoneyAmount";

export default function Donate() {
  const amountDonate = useSelector((state) => state.inputMoneyAmount.amount);
  const flowAmountDonate = useSelector(
    (state) => state.inputMoneyAmount.flowAmount
  );
  const isMonthly = useSelector(state => state.inputMoneyAmount.isMonthlySubscription);
  const dispatch = useDispatch();

  const amount50 = () => {
    dispatch(setFlowAmountMoney(false));
    dispatch(setAmountMoney(50));
  };

  const amount100 = () => {
    dispatch(setFlowAmountMoney(false));
    dispatch(setAmountMoney(100));
  };
  const amount200 = () => {
    dispatch(setFlowAmountMoney(false));
    dispatch(setAmountMoney(200));
  };

  const amount = amountDonate ? amountDonate : flowAmountDonate + "";
  const currency = "USD";
  const style = { layout: "vertical", color: "blue" };

  const ButtonWrapper = ({ currency, showSpinner }) => {
    const [{ options, isPending }, dispatch] = usePayPalScriptReducer();

    useEffect(() => {
      dispatch({
        type: "resetOptions",
        value: {
          ...options,
          currency: currency,
        },
      });
    }, [currency, showSpinner]);

    return (
      <>
        {showSpinner && isPending && <div className="spinner" />}
        <PayPalButtons
          style={style}
          disabled={false}
          forceReRender={[amount, currency, style, isMonthly]}
          fundingSource={undefined}
          createOrder={!isMonthly ? (data, actions) => {
            return actions.order
              .create({
                purchase_units: [
                  {
                    amount: {
                      currency_code: currency,
                      value: amount,
                    },
                  },
                ],
              })
              .then((orderId) => {
                return orderId;
              });
          } : undefined}
          createSubscription={isMonthly ? (data, actions) => {
            return actions.subscription.create({
              plan_id: 'YOUR_PAYPAL_SUBSCRIPTION_PLAN_ID',
            }).then(subscriptionId => {
              console.log('订阅ID:', subscriptionId);
              return subscriptionId;
            });
          } : undefined}
          onApprove={!isMonthly ? (data, actions) => {
            return actions.order.capture().then(function (details) {
              console.log('一次性支付成功:', details);
            });
          } : (data, actions) => {
            return actions.subscription.get().then(details => {
              console.log('订阅创建成功:', details);
            });
          }}
        />
      </>
    );
  };

  return (
    <main className="flex flex-col items-center  w-full h-screen sm:h-auto text-white   bg-[#1b1d33] pt-[8vh] sm:pt-[9vw] lg:pt-[7vw] xl:pt-[5vw] relative ">
      <section className=" flex flex-col items-center justify-center px-[3vw] ">
        <img
          className=" w-full sm:w-[70vw] md:w-[60vw] lg:w-[40vw] xl:w-[30vw]"
          src={imgColageDesk}
          alt="colage"
        />
        <img
          className="w-[10vw] lg:w-[8vw] xl:w-[5vw] h-[10vw] lg:h-[8vw] xl:h-[5vw] mt-10"
          src={imgLogo}
          alt="logo"
        />
        <h3 className="text-xs sm:text-sm md:text-base lg:text-lg mt-5">
          Donate to
        </h3>
        <h1 className="uppercase mt-3 text-sm sm:text-base md:text-lg lg:text-xl font-bold ">
          forward ukraine ministries
        </h1>
        <p className="w-full sm:w-[70vw] md:w-[60vw] lg:w-[40vw] xl:w-[30vw] text-center text-xs sm:text-sm md:text-base lg:text-lg mt-2">
          A humanitarian catastrophe is unfolding in Ukraine as attacks of war
          put millions of lives at risk. Support Military & Refugee
        </p>
        <div className="w-[80vw] sm:w-[50vw] md:w-[30vw] flex justify-around mt-14 mb-4 sm:mt-15 text-black">
          <DonateBTN setAmount={amount50} amount="$50" />
          <DonateBTN setAmount={amount100} amount="$100" />
          <DonateBTN setAmount={amount200} amount="$200" />
          <DonateOther name="Other" />
        </div>

        {/* 月度订阅切换按钮 */}
        <div className="mt-6 mb-4">
          <button 
            onClick={() => dispatch(setIsMonthlySubscription(!isMonthly))}
            className={`px-6 py-2 rounded-lg font-medium ${
              isMonthly ? 'bg-blue-600 hover:bg-blue-700' : 'bg-gray-600 hover:bg-gray-700'
            }`}
          >
            {isMonthly ? '✅ 当前为月度自动捐款' : '🔄 切换为月度自动捐款'}
          </button>
        </div>

        <input
          value={flowAmountDonate}
          onChange={(e) => dispatch(setFlowAmountMoney(e.target.value))}
          onKeyDown={(e) =>
            ["ArrowUp", "ArrowDown", "e", "E", "+", "-", ".", ","].includes(
              e.key
            ) && e.preventDefault()
          }
          className={`${
            flowAmountDonate
              ? "block font-bold w-[80%] sm:w-3/4 md:w-[85%] h-[7vw] sm:h-[6vw] md:h-[4vw]  mb-4 sm:mb-8 text-black rounded-lg  bg-gradient-to-r from-blue-500 to-yellow-500  focus:from-pink-500 focus:to-yellow-500  hover:from-pink-500 hover:to-yellow-500"
              : "hidden"
          }`}
          type="number"
          name=""
        />

        <PayPalScriptProvider
          options={{
            "client-id": "YOUR_PAYPAL_CLIENT_ID",
            components: "buttons",
            currency: "USD",
          }}
        >
          <ButtonWrapper currency={currency} showSpinner={false} />
        </PayPalScriptProvider>
      </section>
    </main>
  );
}

内容的提问来源于stack exchange,提问作者Artem Kosharnyi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 10:21:03