SwiftUI应用从子视图调用StripePaymentView时后端通信异常
SwiftUI集成Stripe支付跳转子视图后后端通信异常排查
问题现象
直接在MySwiftUIApp的WindowGroup中加载StripePaymentView时,支付功能正常运行,后端Express控制台能正确输出创建的PaymentIntent信息;但从应用内其他子视图跳转至StripePaymentView时,后端通信出现错误。
初始正常运行的App代码
import SwiftUI import SwiftData import Stripe @main struct MySwiftUIApp: App { var body: some Scene { WindowGroup { StripePaymentView() //ContentView() /* ResponsiveView {properties in MainTabbedView(layoutProperties: properties) .modelContainer(for: SavedFavBooksFromISBNDB.self) } .onOpenURL { incomingURL in let stripeHandled = StripeAPI.handleURLCallback(with: incomingURL) if (!stripeHandled) { // This was not a Stripe url – handle the URL normally as you would } }*/ } } }
正常运行时后端输出
{paymentIntentID: 'pi_3PxEmnJ15oFjjfNB10B4Cm8o', amount: 7744}
StripePaymentView代码
import SwiftUI import StripePaymentSheet struct StripePaymentView: View { @FocusState var textFieldFocused: Bool @ObservedObject var model = StripePaymentHandler() @State private var enteredNumber = "" var enteredNumberFormatted: Double { return (Double(enteredNumber) ?? 0) / 100 } var body: some View { VStack { Text("输入支付金额") ZStack(alignment: .center) { Text("$\(enteredNumberFormatted, specifier: "%.2f")").font(Font.system(size: 30)) TextField("", text: $enteredNumber, onEditingChanged: { _ in model.paymentAmount = Int(enteredNumberFormatted * 100) }, onCommit: { textFieldFocused = false }).focused($textFieldFocused) .keyboardType(.numberPad) .foregroundColor(.clear) .disableAutocorrection(true) .accentColor(.clear) } Spacer() if let paymentSheet = model.paymentSheet, !textFieldFocused { PaymentSheet.PaymentButton( paymentSheet: paymentSheet, onCompletion: model.onPaymentCompletion ) { payButton } } } .alert(model.alertText, isPresented: $model.showingAlert) { Button("确定", role: .cancel) { } } .onChange(of: textFieldFocused) { if !textFieldFocused { DispatchQueue.global(qos: .background).sync { model.updatePaymentSheet() } } } .onAppear { model.preparePaymentSheet() } .padding(.horizontal) .padding(.top, 50) .padding(.bottom) .toolbar { ToolbarItem(placement: .keyboard) { Button("完成") { textFieldFocused = false } } } } @ViewBuilder var payButton: some View { HStack { Spacer() Text("支付 $\(enteredNumberFormatted, specifier: "%.2f")") Spacer() } .padding() .foregroundColor(.white) .background( RoundedRectangle(cornerRadius: 10, style: .continuous) .fill(.indigo) ) } }
StripePaymentHandler代码
import StripePaymentSheet import SwiftUI class StripePaymentHandler: ObservableObject { @Published var paymentSheet: PaymentSheet? @Published var showingAlert: Bool = false private let backendtUrl = URL(string: "http://18.XXX.XX.XX:3000")! private var configuration = PaymentSheet.Configuration() private var clientSecret = "" private var paymentIntentID: String = "" var alertText: String = "" var paymentAmount: Int = 0 func preparePaymentSheet() { // 从后端获取PaymentIntent和客户信息 let url = backendtUrl.appendingPathComponent("prepare-payment-sheet") var request = URLRequest(url: url) request.httpMethod = "POST" let task = URLSession.shared.dataTask(with: request, completionHandler: { [weak self] (data, response, error) in guard let data = data, let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any], let customerId = json["customer"] as? String, let customerEphemeralKeySecret = json["ephemeralKey"] as? String, let clientSecret = json["clientSecret"] as? String, let paymentIntentID = json["paymentIntentID"] as? String, let publishableKey = json["publishableKey"] as? String, let self = self else { // 处理错误 return } self.clientSecret = clientSecret self.paymentIntentID = paymentIntentID STPAPIClient.shared.publishableKey = publishableKey // 创建PaymentSheet实例 configuration.merchantDisplayName = "Example, Inc." configuration.customer = .init(id: customerId, ephemeralKeySecret: customerEphemeralKeySecret) configuration.allowsDelayedPaymentMethods = true configuration.applePay = .init( merchantId: "merchant.com.your_app_name", merchantCountryCode: "US" ) configuration.returnURL = "your-app://stripe-redirect" }) task.resume() } func updatePaymentSheet() { DispatchQueue.main.async { self.paymentSheet = nil } let bodyProperties: [String: Any] = [ "paymentIntentID": paymentIntentID, "amount": paymentAmount ] let url = backendtUrl.appendingPathComponent("update-payment-sheet") var request = URLRequest(url: url) request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try? JSONSerialization.data(withJSONObject: bodyProperties) request.httpMethod = "POST" let task = URLSession.shared.dataTask(with: request, completionHandler: { [weak self] (data, response, error) in guard let self = self else { // 处理错误 return } DispatchQueue.main.async { self.paymentSheet = PaymentSheet(paymentIntentClientSecret: self.clientSecret, configuration: self.configuration) } }) task.resume() } func onPaymentCompletion(result: PaymentSheetResult) { switch result { case .completed: self.alertText = "支付完成!" case .canceled: self.alertText = "支付已取消!" case .failed(let error): self.alertText = "支付失败:\(error.localizedDescription)" } showingAlert = true } }
后端Express.js代码
const stripe = require('stripe')('sk_test_XXXX'); const express = require('express'); const app = express(); app.use(express.json()); app.post('/prepare-payment-sheet', async (req, res) => { const customer = await stripe.customers.create(); const ephemeralKey = await stripe.ephemeralKeys.create({customer: customer.id}, {apiVersion: '2024-04-10'}); const paymentIntent = await stripe.paymentIntents.create({ amount: 1099, currency: 'usd', customer: customer.id, automatic_payment_methods: { enabled: true, }, }); res.json({ paymentIntentID: paymentIntent.id, clientSecret: paymentIntent.client_secret, ephemeralKey: ephemeralKey.secret, customer: customer.id, publishableKey: 'pk_test_XXXXX' }); }); app.post('/update-payment-sheet', async (req, res) => { const paymentIntent = await stripe.paymentIntents.update( req.body.paymentIntentID, { amount: req.body.amount, } ); console.log(req.body) console.log(res.body) res.json({}); }); app.listen(3000, () => console.log('服务运行在3000端口'));
问题排查与修复方案
核心问题原因
- 异步请求时序冲突:从子视图跳转时,
StripePaymentView的onAppear触发preparePaymentSheet()发起异步请求,但如果用户在请求未完成时(即paymentIntentID还未从后端获取到)就触发updatePaymentSheet(),会导致后端收到空的paymentIntentID,进而触发Stripe API报错。 - 主线程阻塞风险:
onChange中使用DispatchQueue.global.sync调用updatePaymentSheet(),会阻塞主线程,同时加剧异步时序问题。 - 缺乏参数校验:前端未校验
paymentIntentID是否有效就发送请求,后端也未对入参做合法性校验,直接调用Stripe API导致报错。
具体修复步骤
1. 前端添加状态控制与校验
在StripePaymentHandler中添加就绪状态标记,确保preparePaymentSheet完成后才允许执行更新操作:
class StripePaymentHandler: ObservableObject { // 新增就绪状态标记 @Published var isPaymentReady = false // ... 原有代码 func preparePaymentSheet() { // ... 原有请求逻辑 guard let data = data, // ... 原有解析逻辑 let self = self else { return } // ... 原有赋值逻辑 // 请求完成后标记就绪 DispatchQueue.main.async { self.isPaymentReady = true } } func updatePaymentSheet() { // 先校验paymentIntentID是否有效 guard !paymentIntentID.isEmpty else { DispatchQueue.main.async { self.alertText = "支付初始化失败,请重试" self.showingAlert = true } return } // ... 原有更新逻辑 } }
修改StripePaymentView中的触发逻辑,仅在就绪状态下执行更新:
.onChange(of: textFieldFocused) { if !textFieldFocused && model.isPaymentReady { // 替换sync为async避免阻塞主线程 DispatchQueue.global(qos: .background).async { model.updatePaymentSheet() } } }
2. 后端增加参数校验与错误捕获
修改update-payment-sheet接口,增加参数校验和错误处理:
app.post('/update-payment-sheet', async (req, res) => { // 校验必填参数 if (!req.body.paymentIntentID || !req.body.amount) { return res.status(400).json({error: "缺少必填参数"}); } try { const paymentIntent = await stripe.paymentIntents.update( req.body.paymentIntentID, { amount: req.body.amount } ); console.log(req.body); res.json({}); } catch (error) { // 捕获并打印错误,返回友好提示 console.error("更新支付意向失败:", error); res.status(500).json({error: error.message}); } });
内容的提问来源于stack exchange,提问作者user1233894
相关产品推荐
相关产品推荐

