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

如何在Swift的Apple Pay代理方法中向Flutter返回支付状态?

问题描述

我开发了一款Flutter应用,用Swift原生实现Apple Pay功能,已完成Flutter到Swift的支付数据传递。ApplePayContext的didCompleteWith代理方法会返回支付状态("Success"、"Error"、"Cancelled"),但直接调用result(status)无法将状态传回Flutter,需要在代码标记//TODO return result to Flutter的位置实现状态回传。

原代码如下:

import Flutter
import Foundation
import PassKit
import StripeApplePay
import UIKit

var itemName = ""
var price = ""
var freetrialid = -1
var deviceid = "-1"
var optionId = 0
var startDate = ""

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate, ObservableObject, ApplePayContextDelegate {

  static let backendAPIURL = URL(
    string: "xxxxxxxxxxx")!

  @Published var paymentStatus: STPApplePayContext.PaymentStatus?
  @Published var lastPaymentError: Error?

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller: FlutterViewController = window?.rootViewController as! FlutterViewController

    let channel = FlutterMethodChannel(
      name: "methodChannel", binaryMessenger: controller.binaryMessenger)

    channel.setMethodCallHandler { [unowned self] (methodCall, result) in

      if methodCall.method == "payWithApple" {
        guard let args = methodCall.arguments else {
          return
        }

        let myArgs = args as? [String: [String: Any]?]

        for key in myArgs!.keys {
          print(key)
          //not needed
        }

        for value in myArgs!.values {
          print(value as Any)
          itemName = value!["name"] as! String
          price = value!["price"] as! String
          freetrialid = value!["freetrialid"] as! Int
          deviceid = value!["deviceid"] as! String
          optionId = value!["optionId"] as! Int
          startDate = value!["startDate"] as! String
        }

        pay(itemName: itemName, price: price)

      }
    }

    GeneratedPluginRegistrant.register(with: self)

    return true
  }

  func pay(itemName: String, price: String) {
    print("== PAY ==")
    StripeAPI.defaultPublishableKey = "xxxxxxx"
    // Configure a payment request
    let pr = StripeAPI.paymentRequest(
      withMerchantIdentifier: "xxxxxxxxx", country: "AU", currency: "AUD")

    pr.paymentSummaryItems = [
      PKPaymentSummaryItem(label: itemName, amount: NSDecimalNumber(string: price))
    ]

    // Present the Apple Pay Context:
    let applePayContext = STPApplePayContext(paymentRequest: pr, delegate: self)
    applePayContext?.presentApplePay()

  }

  func applePayContext(
    _ context: STPApplePayContext, didCreatePaymentMethod paymentMethod: StripeAPI.PaymentMethod,
    paymentInformation: PKPayment, completion: @escaping STPIntentClientSecretCompletionBlock
  ) {
    // When the Apple Pay sheet is confirmed, create a PaymentIntent on your backend from the provided PKPayment information.
    getClientSecret { secret in
      if let clientSecret = secret["token"] {
        // Call the completion block with the PaymentIntent's client secret.
        completion(clientSecret as! String, nil)
      } else {
        completion(nil, NSError())
      }
    }
  }

  func applePayContext(
    _ context: STPApplePayContext, didCompleteWith status: STPApplePayContext.PaymentStatus,
    error: Error?
  ) {
    switch status {

    case .success:
      // Payment succeeded, show a receipt view
      print("SUCCESS")
      // TODO return result to Flutter
      break

    case .error:
      // Payment failed, show the error
      print("ERROR")
      // TODO return result to Flutter
      break

    case .userCancellation:
      // User canceled the payment
      print("CANCELL")
      // TODO return result to Flutter
      break

    @unknown default:
      fatalError()
    }
  }

  private func getClientSecret(completion: @escaping ([String: Any]) -> Void) {

    let json: [String: Any] = 
      [
        "freetrialid": freetrialid,
        "deviceid": deviceid,
        "optionid": optionId,
        "start_date": startDate,
        "ishiit": false,
        "isflex": true,
        "islime": true,
        "freetrialapp": true,
      ]

    let jsonData = try? JSONSerialization.data(withJSONObject: json)

    let username = "xxxxxxxx"
    let password = "xxxxxxxx"
    let loginString = String(format: "%@:%@", username, password)
    let loginData = loginString.data(using: String.Encoding.utf8)!
    let base64LoginString = loginData.base64EncodedString()

    // create post request
    let url = URL(
      string: "xxxxxxxxx")!  //PUT Your URL
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Accept")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.addValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization")
    // insert json data to the request
    request.httpBody = jsonData

    let task = URLSession.shared.dataTask(with: request) { data, response, error in
      guard let data = data, error == nil else {
        print(error?.localizedDescription ?? "No data")
        return
      }
      let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
      if let responseJSON = responseJSON as? [String: Any] {
        print(responseJSON)  //Code after Successfull POST Request
        completion(responseJSON)
      }
    }

    task.resume()
  }
}

通常我会在pay(itemName: itemName, price: price)方法下返回值,但在此场景下无法实现。


解决方案

核心问题是Apple Pay支付流程是异步的,didCompleteWith代理方法会在支付结束后才触发,而原代码中的result是方法级变量,在payWithApple方法执行完后就会失效。需要把result保存为AppDelegate的实例变量,在代理方法中调用它传递状态。

修改步骤

  1. 在AppDelegate中添加可选的FlutterResult实例变量,用于保存回调
  2. 在payWithApple方法中,将传入的result赋值给该实例变量
  3. 在didCompleteWith代理方法的各个case中,调用保存的result返回对应状态,调用完成后清空变量避免重复调用

修改后的完整代码

import Flutter
import Foundation
import PassKit
import StripeApplePay
import UIKit

var itemName = ""
var price = ""
var freetrialid = -1
var deviceid = "-1"
var optionId = 0
var startDate = ""

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate, ObservableObject, ApplePayContextDelegate {

  static let backendAPIURL = URL(
    string: "xxxxxxxxxxx")!

  @Published var paymentStatus: STPApplePayContext.PaymentStatus?
  @Published var lastPaymentError: Error?
  // 新增:保存Flutter回调的实例变量
  private var paymentResult: FlutterResult?

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller: FlutterViewController = window?.rootViewController as! FlutterViewController

    let channel = FlutterMethodChannel(
      name: "methodChannel", binaryMessenger: controller.binaryMessenger)

    channel.setMethodCallHandler { [unowned self] (methodCall, result) in

      if methodCall.method == "payWithApple" {
        guard let args = methodCall.arguments else {
          result(FlutterError(code: "INVALID_ARGS", message: "参数为空", details: nil))
          return
        }

        let myArgs = args as? [String: [String: Any]?]
        guard let values = myArgs?.values.first, let valueDict = values else {
          result(FlutterError(code: "INVALID_ARGS", message: "参数格式错误", details: nil))
          return
        }

        // 安全解析参数,避免强制解包崩溃
        guard let name = valueDict["name"] as? String,
              let priceStr = valueDict["price"] as? String,
              let ftid = valueDict["freetrialid"] as? Int,
              let did = valueDict["deviceid"] as? String,
              let optId = valueDict["optionId"] as? Int,
              let sDate = valueDict["startDate"] as? String else {
          result(FlutterError(code: "INVALID_ARGS", message: "参数类型错误", details: nil))
          return
        }

        itemName = name
        price = priceStr
        freetrialid = ftid
        deviceid = did
        optionId = optId
        startDate = sDate

        // 保存回调实例
        self.paymentResult = result
        pay(itemName: itemName, price: price)

      }
    }

    GeneratedPluginRegistrant.register(with: self)

    return true
  }

  func pay(itemName: String, price: String) {
    print("== PAY ==")
    StripeAPI.defaultPublishableKey = "xxxxxxx"
    let pr = StripeAPI.paymentRequest(
      withMerchantIdentifier: "xxxxxxxxx", country: "AU", currency: "AUD")

    pr.paymentSummaryItems = [
      PKPaymentSummaryItem(label: itemName, amount: NSDecimalNumber(string: price))
    ]

    let applePayContext = STPApplePayContext(paymentRequest: pr, delegate: self)
    applePayContext?.presentApplePay()

  }

  func applePayContext(
    _ context: STPApplePayContext, didCreatePaymentMethod paymentMethod: StripeAPI.PaymentMethod,
    paymentInformation: PKPayment, completion: @escaping STPIntentClientSecretCompletionBlock
  ) {
    getClientSecret { secret in
      if let clientSecret = secret["token"] {
        completion(clientSecret as! String, nil)
      } else {
        completion(nil, NSError(domain: "PAYMENT_DOMAIN", code: -1, userInfo: [NSLocalizedDescriptionKey: "获取支付凭证失败"]))
      }
    }
  }

  func applePayContext(
    _ context: STPApplePayContext, didCompleteWith status: STPApplePayContext.PaymentStatus,
    error: Error?
  ) {
    defer {
      // 调用完成后清空回调,避免重复调用
      paymentResult = nil
    }

    guard let result = paymentResult else { return }

    switch status {
    case .success:
      print("SUCCESS")
      result("Success")
      break

    case .error:
      print("ERROR")
      if let err = error {
        result(FlutterError(code: "PAYMENT_FAILED", message: err.localizedDescription, details: nil))
      } else {
        result(FlutterError(code: "PAYMENT_FAILED", message: "支付失败", details: nil))
      }
      break

    case .userCancellation:
      print("CANCELL")
      result("Cancelled")
      break

    @unknown default:
      fatalError()
    }
  }

  private func getClientSecret(completion: @escaping ([String: Any]) -> Void) {
    let json: [String: Any] = 
      [
        "freetrialid": freetrialid,
        "deviceid": deviceid,
        "optionid": optionId,
        "start_date": startDate,
        "ishiit": false,
        "isflex": true,
        "islime": true,
        "freetrialapp": true,
      ]

    let jsonData = try? JSONSerialization.data(withJSONObject: json)

    let username = "xxxxxxxx"
    let password = "xxxxxxxx"
    let loginString = String(format: "%@:%@", username, password)
    let loginData = loginString.data(using: String.Encoding.utf8)!
    let base64LoginString = loginData.base64EncodedString()

    let url = URL(string: "xxxxxxxxx")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Accept")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.addValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization")
    request.httpBody = jsonData

    let task = URLSession.shared.dataTask(with: request) { data, response, error in
      guard let data = data, error == nil else {
        print(error?.localizedDescription ?? "No data")
        completion([:])
        return
      }
      let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
      if let responseJSON = responseJSON as? [String: Any] {
        print(responseJSON)
        completion(responseJSON)
      } else {
        completion([:])
      }
    }

    task.resume()
  }
}

额外优化点

  • 增加参数安全解析逻辑,避免强制解包导致崩溃
  • 错误场景返回FlutterError,让Flutter端能捕获具体错误信息
  • 使用defer语句确保回调调用后清空实例变量,防止内存泄漏或重复调用

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 16:05:25