uni离线打包实现 ios 支付StoreKit 2,其实没有想象中那么复杂,不需要写原生插件,不需要转 uts

我走的是原生桥接方法

  1. 在 uni 原生工程项目下新增两个 swift 文件

    1. IAPBridge.swift
    2. IAPManager.swift

      具体两个文件内容如下
      IAPBridge.swift

    import Foundation

    @objc(IAPBridge)
    class IAPBridge: NSObject {
    private var lastResult: String = ""

    复制代码
     @objc func getLastResult() -> String {
         return lastResult
     }
    
     private func runPurchase(productId: String, uuid: String, callback: @escaping (Any?) -> Void) {
         print("[IAP][原生][桥接] 开始购买流程, productId=\(productId), uuid=\(uuid)")
         let unitSep = "\u{001e}"
         let paramBlob = productId + unitSep + uuid
    
         print("[IAP][原生][桥接] 设置参数并启动购买")
         IAPManager.shared.setPurchaseParamBlob(paramBlob)
         IAPManager.shared.startPurchaseFlow()
    
         DispatchQueue.global().async {
             print("[IAP][原生][桥接] 开始等待购买结果")
             let result = IAPManager.shared.waitPurchaseResultMs(120000)
             print("[IAP][原生][桥接] 等待结果返回=\(result)")
             self.lastResult = result
    
             DispatchQueue.main.async {
                 print("[IAP][原生][桥接] 回调给 JS")
                 // 强制桥接为 NSString,避免部分基座把 Swift.String 透传成 undefined
                 callback(result as NSString)
             }
         }
     }
    
     /// JS 调用入口
     @objc func purchase(_ params: NSDictionary, callback: @escaping (Any?) -> Void) {
         print("[IAP][原生][桥接] purchase 被调用, params=\(params)")
    
         guard let productId = params["productId"] as? String, !productId.isEmpty else {
             print("[IAP][原生][桥接] 缺少 productId")
             callback("error: missing productId")
             return
         }
    
         guard let uuid = params["uuid"] as? String, !uuid.isEmpty else {
             print("[IAP][原生][桥接] 缺少 uuid")
             callback("error: missing uuid")
             return
         }
         runPurchase(productId: productId, uuid: uuid, callback: callback)
     }
    
     /// JS 兜底入口:避免 NSDictionary 桥接或 selector 解析失败
     @objc func purchaseWithProductId(_ productId: String, uuid: String, callback: @escaping (Any?) -> Void) {
         print("[IAP][原生][桥接] purchaseWithProductId 被调用, productId=\(productId), uuid=\(uuid)")
         guard !productId.isEmpty else {
             callback("error: missing productId")
             return
         }
         guard !uuid.isEmpty else {
             callback("error: missing uuid")
             return
         }
         runPurchase(productId: productId, uuid: uuid, callback: callback)
     }

    }

IAPManager.swift

复制代码
import Foundation
import StoreKit

/// 不向 JS/UTS 桥传递 **block**,避免 `NSDictionary initWithObjects:... objects[0]=nil` 类崩溃。
/// 流程:`setPurchaseParamBlob` → `startPurchaseFlow` → `waitPurchaseResultMs`(在子线程阻塞等待结果)。
@objc public class IAPManager: NSObject {

    @objc public static let shared = IAPManager()

    private let lock = NSLock()
    private var paramBlobWork = ""
    private var sem: DispatchSemaphore?
    private var outcome: String = "__lxh_iap_not_started__"

    private let unitSep = "\u{001E}"

    /// 仅 NSString,无 block
    @objc public func setPurchaseParamBlob(_ blob: String) {
        print("[IAP][原生][管理器] 设置购买参数 blob=\(blob)")
        lock.lock()
        paramBlobWork = blob
        outcome = "__lxh_iap_not_started__"
        sem = DispatchSemaphore(value: 0)
        lock.unlock()
    }

    /// 无参数,无返回值;在内部 MainActor Task 中跑 StoreKit,结束时 signal
    @objc public func startPurchaseFlow() {
        print("[IAP][原生][管理器] startPurchaseFlow 被调用")
        let semCopy: DispatchSemaphore?
        let blobCopy: String
        lock.lock()
        semCopy = sem
        blobCopy = paramBlobWork
        lock.unlock()

        guard let waitSem = semCopy else { return }

        guard let sepRange = blobCopy.range(of: unitSep) else {
            print("[IAP][原生][管理器] 参数格式无效")
            lock.lock()
            outcome = "error: invalid params"
            lock.unlock()
            waitSem.signal()
            return
        }

        let productId = String(blobCopy[..<sepRange.lowerBound])
        let uuid = String(blobCopy[sepRange.upperBound...])
        print("[IAP][原生][管理器] 解析参数 productId=\(productId), uuid=\(uuid)")
        guard !productId.isEmpty, !uuid.isEmpty else {
            print("[IAP][原生][管理器] 缺少 productId 或 uuid")
            lock.lock()
            outcome = "error: missing productId or uuid"
            lock.unlock()
            waitSem.signal()
            return
        }

        Task { @MainActor in
            print("[IAP][原生][管理器] 开始执行 runPurchase")
            let result = await self.runPurchase(productId: productId, uuid: uuid)
            print("[IAP][原生][管理器] runPurchase 结束, result=\(result)")
            self.lock.lock()
            self.outcome = result
            self.lock.unlock()
            waitSem.signal()
        }
    }

    /// 仅 Int 入参 + NSString 返回,无 block
    @objc public func waitPurchaseResultMs(_ timeoutMs: Int) -> String {
        print("[IAP][原生][管理器] 等待购买结果 timeoutMs=\(timeoutMs)")
        let waitSem: DispatchSemaphore?
        lock.lock()
        waitSem = sem
        lock.unlock()

        guard let s = waitSem else {
            return "error: not prepared"
        }

        let t = max(1, min(timeoutMs, 300_000))
        let deadline = DispatchTime.now() + .milliseconds(t)
        let ok = s.wait(timeout: deadline) == .success
        print("[IAP][原生][管理器] 等待结束 ok=\(ok)")

        lock.lock()
        let text = outcome
        outcome = "__lxh_iap_not_started__"
        sem = nil
        lock.unlock()

        if !ok {
            return "error: timeout"
        }
        return text
    }

    @MainActor
    private func runPurchase(productId: String, uuid: String) async -> String {
        print("[IAP][原生][管理器] runPurchase 入参 productId=\(productId), uuid=\(uuid)")
        do {
            await clearUnfinishedTransactions()
            let products = try await Product.products(for: [productId])
            print("[IAP][原生][管理器] 商品查询数量=\(products.count)")
            guard let product = products.first else {
                return "no product"
            }

            guard let accountToken = UUID(uuidString: uuid) else {
                print("[IAP][原生][管理器] uuid 格式无效=\(uuid)")
                return "error: invalid uuid"
            }
            print("[IAP][原生][管理器] 发起购买 appAccountToken uuid=\(accountToken.uuidString)")
            let result = try await product.purchase(options: [.appAccountToken(accountToken)])

            switch result {
            case .success(let verification):
                print("[IAP][原生][管理器] 购买结果=成功")
                let transaction = try checkVerified(verification)
                let token = transaction.appAccountToken?.uuidString ?? ""
                let msg = """
                success:
                transactionId=\(transaction.id)
                appAccountToken=\(token)
                """
                await transaction.finish()
                return msg

            case .userCancelled:
                print("[IAP][原生][管理器] 购买结果=用户取消")
                return "cancel"

            case .pending:
                print("[IAP][原生][管理器] 购买结果=待处理")
                return "pending"

            default:
                print("[IAP][原生][管理器] 购买结果=未知")
                return "unknown"
            }
        } catch {
            print("[IAP][原生][管理器] runPurchase 异常=\(error.localizedDescription)")
            return "error: \(error.localizedDescription)"
        }
    }

    @MainActor
    private func clearUnfinishedTransactions() async {
        var total = 0
        var finished = 0
        var skipped = 0
        print("[IAP][原生][管理器] 开始清理未完成交易")
        for await pending in Transaction.unfinished {
            total += 1
            do {
                let tx = try checkVerified(pending)
                // 仅作为支付前兜底清理:统一 finish,避免历史交易阻塞
                await tx.finish()
                finished += 1
                print("[IAP][原生][管理器] 已结束未完成交易 id=\(tx.id), productId=\(tx.productID)")
            } catch {
                skipped += 1
                print("[IAP][原生][管理器] 跳过未完成交易(校验失败): \(error.localizedDescription)")
            }
        }
        print("[IAP][原生][管理器] 清理未完成交易结束 total=\(total), finished=\(finished), skipped=\(skipped)")
    }

    private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
        switch result {
        case .verified(let safe):
            return safe
        case .unverified:
            throw NSError(domain: "iap", code: -1)
        }
    }
}

使用方法 关键代码

复制代码
function iapBridgePurchase(productId: string, uuid: string): Promise<string> {
  return new Promise((resolve, reject) => {
    try {
      console.log("[IAP][JS] 开始调用 iapBridgePurchase", { productId, uuid });
      // @ts-ignore APP-PLUS 原生
      const p = typeof plus !== "undefined" ? plus : undefined;
      if (!p?.ios?.newObject) {
        console.error("[IAP][JS] plus.ios 不可用");
        reject(new Error("plus.ios 不可用"));
        return;
      }
      let bridge: any;
      try {
        bridge = p.ios.newObject("IAPBridge");
        console.log("[IAP][JS] 桥接类 = IAPBridge");
      } catch (_) {
        bridge = p.ios.newObject("LxhIapBridge");
        console.log("[IAP][JS] 桥接回退类 = LxhIapBridge");
      }
      let done = false;
      const finish = (...cbArgs: unknown[]) => {
        if (done) return;
        done = true;
        const rawRet =
          cbArgs.length > 0 && cbArgs[0] !== undefined
            ? cbArgs[0]
            : cbArgs.length > 1
              ? cbArgs[1]
              : undefined;
        let rawText = "";
        if (typeof rawRet === "string") {
          rawText = rawRet;
        } else if (rawRet == null) {
          rawText = "";
        } else {
          try {
            rawText = JSON.stringify(rawRet);
          } catch (_) {
            rawText = String(rawRet);
          }
        }
        if (!rawText) {
          try {
            const syncRet = p.ios.invoke(bridge, "getLastResult");
            if (typeof syncRet === "string" && syncRet) {
              rawText = syncRet;
              console.log("[IAP][JS] 回调为空,已通过 getLastResult 获取 =", rawText);
            }
          } catch (e2) {
            console.error("[IAP][JS] getLastResult 读取失败", e2);
          }
        }
        console.log("[IAP][JS] 支付回调参数 =", cbArgs);
        console.log("[IAP][JS] 支付回调原始值 =", rawRet);
        try {
          p.ios.deleteObject(bridge);
        } catch (_) {
          /* ignore */
        }
        resolve(rawText);
      };
      setTimeout(() => {
        if (!done) {
          console.error("[IAP][JS] 桥接回调超时(8秒)");
        }
      }, 8000);
      console.log("[IAP][JS] 调用 purchaseWithProductId:uuid:callback:");
      try {
        p.ios.invoke(bridge, "purchaseWithProductId:uuid:callback:", productId, uuid, finish);
      } catch (e1) {
        console.error("[IAP][JS] 调用 purchaseWithProductId 失败,回退 purchase:callback:", e1);
        const params = p.ios.newObject("NSMutableDictionary");
        p.ios.invoke(params, "setObject:forKey:", productId, "productId");
        p.ios.invoke(params, "setObject:forKey:", uuid, "uuid");
        p.ios.invoke(bridge, "purchase:callback:", params, (rawRet: unknown) => {
          try {
            p.ios.deleteObject(params);
          } catch (_) {
            /* ignore */
          }
          finish(rawRet);
        });
      }
    } catch (e) {
      console.error("[IAP][JS] iapBridgePurchase 异常", e);
      reject(e);
    }
  });
}

具体可以问问 AI

相关推荐
黑化旺仔4 小时前
【OC】Runtime:类元数据、加载初始化与消息派发
开发语言·macos·ios·objective-c
songgeb5 小时前
iOS App支持设置RTL语言的实践
ios·swift·ui kit
软泡芙5 小时前
【iOS】swift启动屏显示黑屏
ios
用户69371750013847 小时前
当你向领导申请 AI 编程工具预算时,如果领导追问“能提效多少,有量化数据吗”,你该怎么回答?
android·后端·ios
Layer7 小时前
一些脱离 Mac 依赖的 Agent 操作 iPhone 实现
ios·ai编程·测试
用户69371750013849 小时前
当我说 Claude 搭配 GLM-5.3-Flash 太好用了,同事却提醒我:你可能正在失去对代码的掌控
android·后端·ios
茶底世界之下9 小时前
一键裁方会丢掉什么?用 Awaking 给园林照片做两种画幅
ios
Marco_Marco13511 小时前
定位SDK接入实战:Android/iOS/鸿蒙三端怎么选、怎么接
android·ios·harmonyos
老李IT笔记1 天前
关掉设备上的 AI 扩展:两个键各管一层,配错了等于没配
人工智能·ios·智能手机
2501_915921431 天前
iOS应用耗电测试:使用Energy Log、Keymob和Instruments进行性能监控与能源消耗分析
android·ios·小程序·https·uni-app·iphone·webview