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

相关推荐
小小码农@16 小时前
Vmware安装MacOS
macos
报错小能手17 小时前
ios开发方向——swift错误处理:do/try/catch、Result、throws
开发语言·学习·ios·swift
开心就好20251 天前
Flutter iOS应用混淆与安全配置详细文档指南
后端·ios
一个人旅程~1 天前
macOS装进移动硬盘成为双系统的操作方法
linux·经验分享·macos·电脑
mCell1 天前
MacOS 下实现 AI 操控电脑(Computer Use)的思考
macos·agent·swift
一个人旅程~1 天前
在M系列的macbook上如何使用VMware安装ARM版的Win11以及注意哪些问题?
linux·windows·经验分享·macos·电脑
开心就好20251 天前
苹果iOS应用开发上架与推广完整教程
后端·ios
用户69371750013841 天前
XChat 为什么选择 Rust 语言开发
android·前端·ios
MonkeyKing1 天前
Objective-C Runtime 完整机制:objc_class /cache/bits 源码解析
前端·ios