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

2023年如何在Stripe定期订阅支付中集成3D Secure认证

Stripe定期订阅集成3D Secure(SCA)解决方案

核心问题分析

你遇到的3D Secure attempt incomplete错误,本质是因为当前代码使用了Stripe旧版的Tokens/Sources API,不支持SCA(强客户认证)规范。真实银行卡需要3D Secure验证时,旧API无法触发完整的验证流程,导致支付失败。


2023年Stripe订阅处理3D Secure的常规方式

  1. 切换到Payment Methods API:替代旧的Tokens/Sources,这是Stripe支持SCA的基础。
  2. 自动生成PaymentIntent:订阅创建/升级涉及即时扣费(如Pro方案首月费用)时,Stripe会自动创建PaymentIntent;若需3D Secure验证,会返回next_action字段。
  3. 前端完成验证:后端将PaymentIntent的client_secret返回给前端,前端调用Stripe的confirmCardPayment完成3D Secure流程。
  4. Webhook确认状态:通过payment_intent.succeeded、subscription.updated等Webhook事件确认最终状态,避免依赖前端返回结果。

代码改造步骤

一、前端重构(使用最新Stripe React库)

首先安装依赖:

npm install @stripe/react-stripe-js @stripe/stripe-js

1. 重构StripeCheckoutForm组件

import React, { useState } from 'react';
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
import { Stack, PrimaryButton, Spinner, Text } from 'office-ui-fabric-react';

const createOptions = () => ({
  style: {
    base: {
      fontSize: '16px',
      color: '#424770',
      '::placeholder': {
        color: '#aab7c4',
      },
    },
    invalid: {
      color: '#c23d4b',
    },
  },
});

interface StripeCheckoutFormProps {
  confirmMessage?: string;
  onPaymentConfirm: () => Promise<string>;
  handlePaymentSuccess: () => void;
}

const StripeCheckoutForm: React.FC<StripeCheckoutFormProps> = ({
  confirmMessage,
  onPaymentConfirm,
  handlePaymentSuccess,
}) => {
  const stripe = useStripe();
  const elements = useElements();
  const [isHandling, setIsHandling] = useState(false);
  const [errorMessage, setErrorMessage] = useState<string | undefined>();

  const handleSubmit = async (ev: React.FormEvent) => {
    ev.preventDefault();
    if (!stripe || !elements) return;

    setIsHandling(true);
    setErrorMessage(undefined);

    try {
      // 调用后端获取PaymentIntent的client_secret
      const clientSecret = await onPaymentConfirm();
      
      // 自动处理3D Secure验证
      const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
        payment_method: {
          card: elements.getElement(CardElement)!,
        },
      });

      if (error) {
        setErrorMessage(error.message);
      } else if (paymentIntent.status === 'succeeded') {
        handlePaymentSuccess();
      }
    } catch (e: any) {
      setErrorMessage(e.message);
    } finally {
      setIsHandling(false);
    }
  };

  return (
    <form style={{ width: 280 }} onSubmit={handleSubmit}>
      <div className="split-form">
        <label style={{ minHeight: 45, minWidth: 200 }}>
          <Text variant="large">Card number</Text>
          <CardElement {...createOptions()} onChange={({ error }) => error && setErrorMessage(error.message)} />
        </label>
      </div>
      <div className="error" role="alert">
        {errorMessage}
      </div>
      <Stack styles={{ root: { marginTop: 10 } }}>
        {isHandling ? (
          <PrimaryButton disabled>
            <Spinner></Spinner>
          </PrimaryButton>
        ) : (
          <PrimaryButton type="submit" style={{ width: '100%' }}>
            {confirmMessage || 'Pay'}
          </PrimaryButton>
        )}
      </Stack>
    </form>
  );
};

export default StripeCheckoutForm;

2. 重构SubscribePay组件

import React from 'react';
import { connect } from 'dva';
import { State as ReduxState } from '../../../store/reducer';
import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import { ContentCard } from '../common';
import { Card } from '@uifabric/react-cards';
import StripeCheckoutForm from '../StripeCheckoutForm';
import { Dispatch } from 'redux';
import selectors from '../../../selectors';
import { Plan } from '../../../services/subscription';

// 初始化Stripe实例
const stripePromise = loadStripe(STRIPE_PUBLISHABLE_KEY);

interface Props {
  dispatch: Dispatch<any>;
  selectedPlans: Plan[];
  goCompleted: any;
  confirmMessage: string;
}

class SubscribePay extends React.Component<Props> {
  // 调用后端创建/升级订阅,获取PaymentIntent的client_secret
  onPaymentConfirm = async () => {
    const response = await fetch('/httpOnly/stripe/subscriptions/upgrade', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${localStorage.getItem('token')}` // 带上用户登录凭证
      },
      body: JSON.stringify({
        plans: this.props.selectedPlans,
      }),
    });

    const data = await response.json();
    if (!response.ok) throw new Error(data.message);
    
    return data.client_secret;
  };

  handlePaymentSuccess = () => {
    this.props.goCompleted();
  };

  render() {
    const { selectedPlans, confirmMessage } = this.props;
    
    if (!selectedPlans.length) {
      return <div>There is no selectedPlans. Go Back</div>;
    }

    return (
      <div style={{ display: 'flex', justifyContent: 'center', marginTop: "10px" }}>
        <Elements stripe={stripePromise}>
          <div>
            <div style={{ width: '100%', textAlign: 'center', marginBottom: '10px' }}>
              <span style={{
                fontSize: '20px',
                fontFamily: "Segoe UI, Segoe UI Web (West European), Segoe UI, -apple-system, BlinkMacSystemFont, Roboto, Helvetica Neue, sans-serif",
              }}>Pay with a bank card</span>
            </div>
            <ContentCard>
              <Card.Item styles={{ root: { margin: 'auto', paddingTop: 10, paddingBottom: 10 } }}>
                <StripeCheckoutForm
                  confirmMessage={confirmMessage}
                  onPaymentConfirm={this.onPaymentConfirm}
                  handlePaymentSuccess={this.handlePaymentSuccess}
                />
              </Card.Item>
            </ContentCard>
          </div>
        </Elements>
      </div>
    );
  }
}

export default connect((state: ReduxState) => ({
  plans: selectors.subscription.getPlans(state),
}))(SubscribePay);

二、后端代码改造

router.post(
  "/httpOnly/stripe/subscriptions/upgrade",
  loginCheckCallback,
  plansCheckCallback,
  async (req, res) => {
    const user = res.locals.user;
    const plans = res.locals.plans;
    try {
      const customer = await getOrCreateCustomer(user);

      // 获取用户活跃订阅
      const subscriptions = await stripe.subscriptions.list({
        customer: customer.id,
        status: 'active',
      });

      let subscription;
      let clientSecret;
      
      // 查找标记为"Store"的订阅
      subscription = subscriptions.data.find((sub) => {
        return sub.metadata.label === "Store";
      });

      if (subscription === undefined) {
        // 创建新订阅(Pro方案)
        const subscriptionResult = await stripe.subscriptions.create({
          customer: customer.id,
          items: plans.map(x => ({ price: x.id })), // 使用Price ID替代旧版Plan ID
          payment_behavior: 'default_incomplete', // 关键:生成未完成的PaymentIntent等待验证
          expand: ['latest_invoice.payment_intent'], // 展开获取PaymentIntent信息
          metadata: { label: 'Store' },
        });
        
        subscription = subscriptionResult;
        clientSecret = subscriptionResult.latest_invoice.payment_intent.client_secret;
      } else {
        // 升级现有订阅到Pro方案
        const subscriptionResult = await stripe.subscriptions.update(subscription.id, {
          items: plans.map(x => ({ 
            id: subscription.items.data[0].id, 
            price: x.id 
          })),
          billing_cycle_anchor: 'now',
          proration_behavior: 'none',
          cancel_at_period_end: false,
          payment_behavior: 'default_incomplete', // 升级费用需验证
          expand: ['latest_invoice.payment_intent'],
        });
        
        subscription = subscriptionResult;
        clientSecret = subscriptionResult.latest_invoice.payment_intent.client_secret;
      }

      res.json({ subscription, client_secret: clientSecret, created: true });
    } catch (e) {
      console.log(e);
      res
        .status(e.statusCode || 500)
        .json({ message: "error when create subscriptions: " + e.message });
    }
  }
);

关键注意事项

  1. Webhook配置:必须监听以下Stripe事件确保流程可靠:
    • payment_intent.succeeded:确认付款成功
    • subscription.updated:确认订阅状态更新
    • payment_intent.payment_failed:处理付款失败
  2. 测试3D Secure:使用Stripe测试卡4000000000003220可触发验证流程
  3. Price ID替代Plan ID:新版Stripe API推荐使用Price ID(每个Plan对应多个Price,如不同货币/周期),确保plans中的id为Price ID
  4. SCA自动豁免:Stripe会自动判断是否可豁免3D Secure,无需手动配置

针对你的问题的直接回答

  1. 更新订阅时在哪里创建PaymentIntent?
    无需手动创建。调用stripe.subscriptions.update并设置payment_behavior: 'default_incomplete'时,Stripe会自动为升级费用生成PaymentIntent,通过expand参数获取其client_secret返回给前端即可。

  2. 2023年启用3D Secure的常规方式?
    切换到Payment Methods API,订阅操作时使用payment_behavior: 'default_incomplete',让Stripe自动生成PaymentIntent,前端通过confirmCardPayment完成验证,同时依赖Webhook确认最终状态。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 08:52:36