Stripe订阅支付后无法更新为ACTIVE状态问题排查
我用Java+React开发固定价格订阅功能,参照Stripe官方固定价格订阅示例开发,但遇到问题:客户完成卡片支付后,Stripe后台显示支付已处理,但订阅仍处于INCOMPLETE状态。已经添加了ON_SUBSCRIPTION设置,问题依旧。相关代码如下:
Java后端代码
@GetMapping("/stripe/prices") public Response<PricesResponse> prices() { RequestOptions options = RequestOptions.builder() .setApiKey(apiKey) .build(); // search customer try { CustomerSearchParams params = CustomerSearchParams.builder() .setQuery("name:'aaa bbb'") .build(); CustomerSearchResult customers = Customer.search(params, options); List<String> names = customers.getData() .stream() .map(Customer::getName) .toList(); logger.info(String.valueOf(names)); } catch (StripeException e) { throw new RuntimeException(e); } // create customer Customer customer; try { String customerName = "Jenny Rosen " + System.currentTimeMillis(); System.out.println(customerName); CustomerCreateParams params = CustomerCreateParams.builder() .setName(customerName) .setEmail("jennyrosen@example.com") .build(); customer = Customer.create(params, options); } catch (StripeException e) { throw new RuntimeException(e); } // get prices PriceCollection prices = new PriceCollection(); try { PriceListParams params = PriceListParams .builder() .build(); prices = Price.list(params, options); } catch (StripeException e) { throw new RuntimeException(e); } // create subscriptions Subscription subscription; try { SubscriptionCreateParams.PaymentSettings paymentSettings = SubscriptionCreateParams.PaymentSettings .builder() .setSaveDefaultPaymentMethod(SubscriptionCreateParams.PaymentSettings.SaveDefaultPaymentMethod.ON_SUBSCRIPTION) .build(); SubscriptionCreateParams subCreateParams = SubscriptionCreateParams .builder() .setCustomer(customer.getId()) .addItem( SubscriptionCreateParams .Item.builder() .setPrice(prices.getData().get(0).getId()) .build() ) .setPaymentSettings(paymentSettings) .setPaymentBehavior(SubscriptionCreateParams.PaymentBehavior.DEFAULT_INCOMPLETE) .addAllExpand(Arrays.asList("latest_invoice.payment_intent")) .build(); subscription = Subscription.create(subCreateParams, options); } catch (StripeException e) { throw new RuntimeException(e); } // create payment intent PaymentIntent paymentIntent; try { PaymentIntentCreateParams params = PaymentIntentCreateParams.builder() .setCustomer(customer.getId()) .setAmount(prices.getData().get(0).getUnitAmount()) .setCurrency("usd") .build(); paymentIntent = PaymentIntent.create(params, options); } catch (StripeException e) { throw new RuntimeException(e); } return new Response<>(new PricesResponse(publishableKey, paymentIntent.getClientSecret(), prices.getData() .stream() .map(price -> new PriceResponse(price.getId(), price.getNickname(), price.getUnitAmount())) .toList())); }
React前端代码
价格选择页面
const Prices = () => { const navigate = useNavigate(); const [prices, setPrices] = useState([]); const [clientSecret, setClientSecret] = useState(""); useEffect(() => { doRestCall('/stripe/prices', 'get', null, null, (response) => { setPrices(response.body.prices) setClientSecret(response.body.clientSecret) }) }, []) function toCheckout() { navigate('/checkout', { state: { clientSecret } }) } return ( <div> <h1>Select a plan</h1> <div className="price-list"> {prices.map((price) => { return ( <div key={price.id}> <h3>{price.name}</h3> <p> ${price.amount / 100} / month </p> <button onClick={() => toCheckout()}> Select </button> </div> ) })} </div> </div> )}
结账页面
const Checkout = () => { const { state: { clientSecret, } } = useLocation(); const stripe = useStripe(); const elements = useElements(); const [name, setName] = useState('Jenny Rosen'); const [messages, setMessages] = useState(''); const navigate = useNavigate(); const handleSubmit = async (e) => { e.preventDefault(); const cardElement = elements.getElement(CardElement); const { error } = await stripe.confirmCardPayment(clientSecret, { payment_method: { card: cardElement, billing_details: { name: name, } } }); if(error) { // show error and collect new card details. setMessages(error.message); return; } navigate('/complete', { state: { clientSecret } }); }; return (<> <h1>Subscribe</h1> <p> Try the successful test card: <span>4242424242424242</span>. </p> <p> Try the test card that requires SCA: <span>4000002500003155</span>. </p> <p> Use any <i>future</i> expiry date, CVC,5 digit postal code </p> <hr/> <form onSubmit={handleSubmit}> <label> Full name <input type="text" id="name" value={name} onChange={(e) => setName(e.target.value)}/> </label> <CardElement/> <button> Subscribe </button> <div>{messages}</div> </form> </>);
另外,关于Stripe订阅流程中提到的Invoice,是否需要手动处理?我在Java示例中看到/invoice-preview接口,但未找到React端的调用位置。
1. 订阅状态INCOMPLETE的核心原因
你当前代码的关键错误是:手动创建了独立的PaymentIntent,而非使用订阅创建时自动生成的Invoice关联的PaymentIntent。
当设置PaymentBehavior.DEFAULT_INCOMPLETE创建订阅时,Stripe会自动生成对应Invoice和关联的PaymentIntent,你需要将这个PaymentIntent的clientSecret返回给前端,而非重新创建一个无关的支付意图。前端完成独立PaymentIntent的支付后,Stripe无法将其关联到订阅的Invoice,导致订阅一直处于INCOMPLETE状态。
修复步骤
修改Java后端代码,移除手动创建PaymentIntent的逻辑,直接从订阅中提取关联的PaymentIntent的clientSecret:
// 移除手动创建PaymentIntent的代码块 /* PaymentIntent paymentIntent; try { PaymentIntentCreateParams params = PaymentIntentCreateParams.builder() .setCustomer(customer.getId()) .setAmount(prices.getData().get(0).getUnitAmount()) .setCurrency("usd") .build(); paymentIntent = PaymentIntent.create(params, options); } catch (StripeException e) { throw new RuntimeException(e); } */ // 从订阅中提取关联的PaymentIntent的clientSecret String clientSecret = null; Invoice latestInvoice = subscription.getLatestInvoice(); if (latestInvoice != null && latestInvoice.getPaymentIntent() != null) { clientSecret = latestInvoice.getPaymentIntent().getClientSecret(); } // 返回使用该clientSecret return new Response<>(new PricesResponse(publishableKey, clientSecret, prices.getData() .stream() .map(price -> new PriceResponse(price.getId(), price.getNickname(), price.getUnitAmount())) .toList()));
前端代码无需修改,确保接收的是订阅关联的PaymentIntent的clientSecret即可。
2. Invoice相关疑问
是否需要手动处理Invoice?
固定价格订阅的常规流程中,Stripe会自动处理Invoice的生成、付款尝试和状态更新,无需手动干预。仅当出现付款失败(如卡片过期、余额不足)时,需通过Webhook监听invoice.payment_failed事件,提醒用户更新支付方式。关于
/invoice-preview接口
该接口用于在用户选择订阅方案后预览即将生成的Invoice(如显示税费、折扣后的最终金额),属于可选体验优化步骤。官方示例未在React端调用,是因为这不是流程必需环节。若需添加,可在用户选择价格后调用后端/invoice-preview接口,传入Customer ID和Price ID,后端调用Stripe的Invoice.create()并设置preview=true生成预览,再将数据返回前端展示。
内容的提问来源于stack exchange,提问作者Alex

