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

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端口'));

问题排查与修复方案

核心问题原因

  1. 异步请求时序冲突:从子视图跳转时,StripePaymentView的onAppear触发preparePaymentSheet()发起异步请求,但如果用户在请求未完成时(即paymentIntentID还未从后端获取到)就触发updatePaymentSheet(),会导致后端收到空的paymentIntentID,进而触发Stripe API报错。
  2. 主线程阻塞风险:onChange中使用DispatchQueue.global.sync调用updatePaymentSheet(),会阻塞主线程,同时加剧异步时序问题。
  3. 缺乏参数校验:前端未校验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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 16:59:53