iOS 苹果内购 Storekit 2

StoreKit 2 介绍

苹果内购的 StoreKit 2 引入了一套更安全、更现代化的订单凭证校验机制,核心是使用 JWS ( JSON Web Signature) 格式的签名数据,并提供了客户端本地验证和服务端通过 API 验证两种方式。

StoreKit 2 为 iOS/macOS 等平台的应用内购买带来了许多显著的改进。下面这个表格汇总了它的主要优点:

优势领域 主要优点
​​开发体验与API设计​​ 基于 Swift 并发模型,API 更简洁直观
​​安全与验证​​ 引入 JWS 格式,支持客户端本地验证
​​功能与集成​​ 新增应用内退款、订阅管理、交易历史查询等功能
​​服务器支持​​ 提供 App Store Server API,支持服务器端验证和订阅状态查询
​​测试与调试​​ 改进沙盒测试环境,支持配置 StoreKit 配置文件进行本地测试

下面我们来详细了解一下这些优点:

🛠️ 开发体验与 API 设计

StoreKit 2 充分利用了 Swift 并发特性(async/await) ,使得编写异步代码更加清晰简洁。例如,获取产品信息只需一行代码 let products = try await Product.products(for: productIDs),发起购买也简化为 let result = try await product.purchase(),这避免了以往复杂的回调嵌套,大大提升了代码的可读性和可维护性。

新的 API 设计更加模块化和直观 ,提供了 ProductTransaction 等类型,让开发者能更轻松地获取产品信息、处理交易和管理订阅。

🔒 安全与订单验证

StoreKit 2 引入了 JWS( JSON Web Signature)格式 的签名交易信息。每笔交易都有一个对应的 JWS 对象,这使得客户端本地验证订单成为可能,无需每次都连接苹果服务器,从而简化了验证流程并降低了因网络问题导致的验证失败风险。

虽然客户端可以本地验证,但 StoreKit 2 也提供了强大的 App Store Server API,允许你的服务器直接与苹果服务器通信,查询交易历史、订阅状态等信息,确保了服务器端验证的可靠性。

📊 功能与集成增强

StoreKit 2 支持直接在应用中请求退款和管理订阅。用户无需离开应用或前往系统设置,即可申请退款或管理他们的订阅,提升了用户体验和应用的集成度。

通过 Transaction.currentEntitlements API,开发者可以轻松获取用户当前有效的订阅和非消耗型产品授权,简化了恢复购买的流程

新的 Product 类型直接提供了商品类型信息(如消耗型、非消耗型、自动续期订阅等),并且可以检查用户是否有资格享受 introductory offers(推广优惠),帮助开发者更准确地展示商品信息和定价。

🌐 服务器端支持

App Store Server API 允许你的服务器主动查询用户的交易历史和订阅状态,即使错过了苹果的服务器通知,也能及时同步用户的最新状态。

苹果为沙盒环境提供了独立的服务器通知配置选项,方便开发者更好地测试退款等相关通知。

⚙️ 测试与调试

开发者可以在 Xcode 中创建 StoreKit 配置文件,在没有真实网络连接的情况下配置和测试应用内购买项目,简化了测试流程。

⚠️ 需要注意的点

StoreKit 2 也有一些限制:

  • 仅支持 Swift:StoreKit 2 是基于 Swift 的新特性构建的。
  • 最低系统要求:要求 iOS 15、iPadOS 15、tvOS 15 或 watchOS 8 及以上版本。如果你的应用需要支持更早的系统版本,可能需要同时维护 StoreKit 1 和 StoreKit 2 的代码,或者继续使用 StoreKit 1。

💡 总结一下: StoreKit 2 通过现代化的 Swift API、简化的购买和验证流程、增强的服务器支持以及更强大的功能(如应用内退款和管理),显著提升了开发者和用户在应用内购买方面的体验。如果你的应用目标系统版本在 iOS 15 及以上,采用 StoreKit 2 会是一个不错的选择。

下面是一个对比表格,帮助你快速了解 StoreKit V1 和 V2 在订单校验上的主要区别:

特性 StoreKit V1 (旧版) StoreKit V2 (新版)
​​凭证格式​​ 应用收据 (App Receipt) JWS (JSON Web Signature) 签名数据
​​验证方式​​ 客户端将收据(Base64)发送至自家服务器,服务器调用苹果接口验证 ​​1. 客户端本地验证​​ ​​2. 服务端直连Apple验证​​
​​异步通知​​ ❌ 支持差,易掉单 ✅ ​​Server Notification V2​​,苹果主动推送
​​环境区分​​ production / sandbox production / sandbox + Apple TestFlight
​​幂等性​​ 需自行实现,难度大 transactionId 唯一,全局可幂等
​​安全性​​ 客户端可伪造请求,风险较高 服务端直连 Apple 验证 + JWT 签名,安全性高
​​订单关联​​ 依赖 applicationUsername (易丢失) 使用 ​​appAccountToken​​ (UUID格式,可靠绑定)

StoreKit V2 的校验流程可以概括为以下几步:

以下是校验订单凭证的关键环节和推荐实践:

🔍 客户端本地验证

在客户端,当支付完成后,你可以直接从 VerificationResult 中获取验证结果:

swift 复制代码
// 发起支付(示例代码片段)
let result = try await product.purchase()

// 处理支付结果
switch result {
case .success(let verificationResult):
    // 检查验证结果
    switch verificationResult {
        case .verified(let transaction): 
            // ✅ JWS 验证通过,可以放心交付商品
            print("订单验证成功: (transaction.transactionID)")
            await transaction.finish() // 完成交易
        case .unverified(let transaction, let error): 
            // ❌ JWS 验证失败,存在安全风险,不应交付商品
            print("订单验证失败: (error.localizedDescription)")
    }
case .userCancelled: 
    break // 用户取消
case .pending: 
    break // 交易 pending
@unknown default: 
    break
}

此验证利用本地密码学方法检查 JWS 的签名是否由苹果签发,能快速识别篡改,但最终仍需服务端进行二次验证以确保绝对安全。

🌐 服务端验证

服务端验证是最终保证交易安全性和可靠性的关卡。绝对不要只依赖客户端验证。

  1. 获取 transactionId :客户端将支付成功后获取的 transactionId 发送给你的服务端。
  2. 服务端调用苹果 API :你的服务端使用苹果提供的 AppStoreServerAPIClient 等库,携带必要的认证信息(如 Issuer ID、Key ID、私钥等)向苹果的服务器接口(https://api.storekit.itunes.apple.com)发起请求,查询该 transactionId 的详细状态和信息。
  3. 处理响应 :苹果服务器会返回交易的详细状态(如 0 表示有效)、商品信息、购买时间等。你的服务端需根据这些信息完成商品发放,并做好幂等处理(因为客户端和苹果通知可能重复调用)。

处理环境区分

苹果沙盒环境 (Environment.SANDBOX) 和正式环境 (Environment.PRODUCTION) 是隔离的。一个常见的实践是优先用正式环境验证,若失败则尝试沙盒环境:

typescript 复制代码
// 示例代码片段:服务端尝试不同环境
public String getAppAccountToken(String transactionId, Environment environment) {
  try {
    // 创建客户端并指定环境
    AppStoreServerAPIClient client = new AppStoreServerAPIClient(encodedKey, keyId, issuerId, bundleId, environment);
    TransactionInfoResponse transactionInfo = client.getTransactionInfo(transactionId);
    // ... 验证和解码 payload ...
  } catch (APIException e) {
    if (e.getApiError().errorCode() == 4040010L) { // 订单不存在于此环境
      return "";
    }
    throw e;
  }
}

📨 处理苹果服务器通知 (Server Notifications)

为了最大限度防止掉单 ,强烈建议在苹果开发者后台配置一个服务器端点(URL) 来接收 Server Notification V2

  • 配置方式:在 App Store Connect 中为你的 App 配置一个用于接收异步通知的服务器地址。

  • 工作流程 :当交易状态发生变化(如购买、续订、退款),苹果服务器会向该地址发送 POST 请求, payload 是一个 signedPayload 字符串。

  • 服务端处理:你的服务端需要:

    • 验证 signedPayload 的 JWS 签名以确保其确实来自苹果。
    • 解析 payload,获取交易信息(如 transactionIdnotificationType)。
    • 根据 notificationType(如 DID_CHANGE_RENEWAL_PREFERENCE, REFUND)进行相应业务处理(如更新订阅状态、撤销权益)。
    • 同样做好幂等处理,因为通知可能重试。

💡 最佳实践与注意事项

  1. 关联自有订单系统 :在发起购买时,强烈建议通过 PurchaseOption.appAccountToken(yourOrderUUID) 传入一个 UUID(如你自有订单系统的订单号)。这个令牌会永久保存在交易信息中,无论通过客户端还是服务端验证都能获取到,是解决掉单和准确补单的关键
  2. 兼容旧版本 iOS :StoreKit 2 仅支持 iOS 15.0+。如需支持更低系统版本,需同时实现 StoreKit 1 的校验流程,并注意 applicationUsername 在 StoreKit 1 中可能不可靠。
  3. 安全第一 :所有关键的商品发放和权限开通逻辑都应放在服务端。客户端传来的任何数据(包括 transactionId)都只能作为查询依据,绝不能作为可信凭据
  4. 日志与监控:记录验证请求、响应和苹果通知的完整日志,并设置报警用于监控验证失败率和通知异常,便于快速发现和排查问题。

总之,StoreKit 2 的订单校验更现代化也更安全。核心是采用客户端快速验证与服务端权威验证相结合,并积极配合苹果的服务器通知机制 ,同时用好 appAccountToken 来关联订单,这样才能构建一个健壮、可靠的 iOS 内购系统。

🛠️ 使用 "Get Transaction Info"

服务端通过 transactionId 查询苹果订单详细状态和信息,调用的具体 API 是 Get Transaction Info

"Get Transaction Info" 接口属于 StoreKit 2 (App Store Server API) 生态的一部分,主要用于服务器端查询交易信息。它不属于传统的 StoreKit 1 (原始 API)。

为了更清晰地展示区别,我准备了一个表格:

特性维度 StoreKit 1 (原始 API) StoreKit 2 (App Store Server API, 包含 Get Transaction Info)
​​API 类型​​ 主要是一套客户端 API (如 SKProductsRequest, SKPaymentQueue),服务器端通过验证​​应用收据​​(App Receipt)来获取交易信息 主要是一套​​服务器端 REST API​​,让开发者的后端服务器能直接向苹果服务器查询特定交易或订单的详细信息
​​数据格式​​ 使用应用收据(二进制或 Base64 编码字符串) 使用 ​​JWS (JSON Web Signature)​​ 格式的签名数据,信息更结构化,包含在 signedTransactionInfo 字段中
​​获取交易信息的方式​​ 客户端需要提供整个应用收据,服务器解析收据来获取所有交易 服务器可直接使用 ​​transactionId​​ 调用 GET api.storekit.itunes.apple.com/inApps/v1/t... 来查询特定交易
​​关键标识符​​ 主要依赖 transactionIdentifier (但需注意其可能变化) 引入了 ​​appAccountToken​​ (UUID格式),由开发者在购买时传入,用于可靠关联用户和订单,能有效解决掉单问题
​​环境​​ 通过向苹果的验证端点发送收据时指定 sandbox 参数来区分环境 使用不同的​​基础 URL​​ 区分环境: 生产环境: api.storekit.itunes.apple.com 沙盒环境: api.storekit-sandbox.itunes.apple.com
​​主要用途​​ 验证应用内所有交易的完整性,恢复购买,支持旧版 iOS 系统 服务器端精确查询订单、处理退款、查询订阅状态、处理消费信息等,为服务器提供更强大的订单管理能力

"Get Transaction Info" API 允许你的服务器通过一个具体的 transactionId,向苹果服务器查询该笔交易的详细、经过签名验证的信息 。

  • 基本请求格式 GET ``https://api.storekit.itunes.apple.com/inApps/v1/transactions/{transactionId} 你需要将 {transactionId} 替换为具体的交易 ID。

  • 认证方式 :调用此 API 必须在请求头中携带使用 ES256 算法签名的 JWT Token 。生成这个 Token 需要:

    • Issuer ID:从 App Store Connect 的密钥页面获取。
    • Key ID:在 App Store Connect 中生成 App Store Server API 密钥时获得。
    • 私钥文件 :生成上述密钥时下载的 .p8 文件。此私钥需妥善保管,用于签名 JWT 。
  • 响应数据 :API 返回的响应体中包含一个 signedTransactionInfo 字段,其值是 JWS 格式的字符串。你的服务器需要对此 JWS 进行解码和验证,以获取明文的交易信息载荷(Payload),其中包含商品 ID、购买日期、原始交易 ID、价格、货币等信息 。

💡 提示

  • 兼容性 :StoreKit 2 的服务器 API(包括 Get Transaction Info)主要是为了增强服务器端的处理能力,并不能直接替代客户端的支付流程。在实际项目中,客户端可能仍需根据系统版本兼容 StoreKit 1 和 StoreKit 2 的支付接口 。
  • 错误处理 :如果使用生产环境 URL 调用此 API 返回错误码 4040010(TransactionIdNotFoundError),表明该交易 ID 在生产环境中不存在,应尝试使用沙盒环境 URL 再次调用 。

下面是该接口的核心信息、调用方法以及一些注意事项的汇总:

项目 说明
​​API 名称​​ Get Transaction Info
​​官方文档​​ Apple Developer Documentation
​​功能描述​​ 根据单个 transactionId 查询某次特定交易的详细信息。
​​HTTP 方法与端点​​ GET api.storekit.itunes.apple.com/inApps/v1/t...
​​请求参数​​ 路径参数 transactionId
​​认证方式​​ 需在请求头 Authorization 中携带使用 ES256 算法签名的 JWT Token
​​响应内容​​ 包含交易信息的 JWS (JSON Web Signature) 格式数据,需解码验证 。

🔑 调用前的准备

调用此 API 前,你需要在服务端配置好身份认证凭证 :

  1. Issuer ID:在 App Store Connect 的 "密钥" 页面查找。
  2. Key ID:在 App Store Connect 中生成 App Store Server API 密钥时获得。
  3. 私钥文件 :生成上述密钥时下载的 .p8 文件。请妥善保管,苹果不保存副本。

📡 发起请求

使用上述凭证生成 JWT 后,即可调用 API。以下是使用 Python 示例的代码片段 :

ini 复制代码
import requests
import json

# 1. 生成 JWT Token (示例,具体实现依赖你的JWT库和密钥)
token = generate_jwt_token()  # 你需要实现此函数,参考中的Python示例

# 2. 设置请求头和URL
transaction_id = "THE_TRANSACTION_ID_TO_QUERY"  # 替换为具体的 transactionId
url = f"https://api.storekit.itunes.apple.com/inApps/v1/transactions/{transaction_id}"
headers = {"Authorization": f"Bearer {token}"}

# 3. 发送 GET 请求
response = requests.get(url, headers=headers)
data = response.json()

# 4. 处理响应
if response.status_code == 200:
    # 成功响应,data 中包含 signedTransactionInfo(JWS格式)
    signed_transaction_info = data['signedTransactionInfo']
    # 你需要对 JWS 进行解码和验证以获取交易详情
    transaction_payload = decode_and_verify_jws(signed_transaction_info)  # 需要实现解码验证
    print("交易信息:", transaction_payload)
else:
    print("请求失败,状态码:", response.status_code)
    print("错误信息:", data)

⚠️ 重要注意事项

  • 环境区分

    • 生产环境 :使用基础 URL https://api.storekit.itunes.apple.com
    • 沙盒环境 :使用基础 URL https://api.storekit-sandbox.itunes.apple.com
    • 一个常见的实践是,如果无法确定交易所属环境,可先尝试向生产环境发起请求,如果返回错误码 4040010(表示订单不存在),再尝试沙盒环境 。
  • 响应数据验证 :API 返回的 signedTransactionInfo 是 JWS 格式,务必在服务端对其进行解码和签名验证,以确保数据确实来自苹果且未被篡改 。

  • 错误处理 :务必做好网络请求和 API 返回错误码的处理。详细的错误码可查阅 Apple 官方文档

  • 使用官方库简化流程 :苹果提供了开源的 App Store Server Library(支持多种语言),可用于简化 JWT 生成、API 调用以及 JWS 响应验证等过程 。推荐使用。

⚠️ 问题?

💡 除了查询单次交易,还能做什么?

App Store Server API 还提供了其他强大的功能,例如 :

  • Get Transaction History :获取某个用户(由其 originalTransactionId 标识)在所有设备上的历史交易记录。

  • Look Up Order ID:根据用户提供的订单号(Order ID,可在苹果发送的收据邮件中找到)来查询交易,常用于客服处理用户补单需求。

💡 Storekit 2 是不是就没有漏单的可能了?

StoreKit 2 通过一系列的设计改进,极大地降低了内购过程中"漏单"的可能性,但并不能完全绝对地保证 100% 不会发生。它通过更可靠的机制,让开发者能更容易地发现和处理异常情况。

为了更直观地对比 StoreKit 1 和 StoreKit 2 在防止漏单方面的差异,我整理了下面的表格:

防漏单机制 StoreKit 1 (旧版) StoreKit 2 (新版)
​​核心凭证​​ 应用收据 (App Receipt) ​​JWS 签名交易数据​​ (每笔交易独立可验证)
​​验证方式​​ 服务端需主动频繁轮询验证整个收据 ​​客户端本地快速验证​​ + ​​服务端权威验证​​ + ​​Apple服务器主动异步通知 (Server Notifications)​​
​​订单关联标识​​ applicationUsername (可选,且易丢失或不可靠) ​​appAccountToken​​ (UUID格式,强烈建议使用,用于可靠关联用户和订单)
​​事务状态管理​​ 相对模糊,依赖开发者自行处理 更清晰的事务状态 (purchased, revoked 等),并提供查询历史交易的API
​​幂等性处理​​ 需开发者自行实现,难度较大 ​​transactionId 全局唯一​​,服务端可依赖此ID实现天然幂等性,避免重复发货

从表格可以看出,StoreKit 2 从设计之初就针对 StoreKit 1 中可能导致漏单的薄弱环节进行了加强。

🔧 StoreKit 2 降低漏单风险的关键改进

  1. 可靠的订单关联标识 ( appAccountToken ) :这是最重要的改进之一。在发起购买时,你可以传入一个你自己生成的 UUID(比如与你服务器订单号绑定)。这个令牌会永久保存在交易信息中,无论通过哪种方式验证(客户端、服务端、通知)都能获取到,为补单和对账提供了唯一可靠的依据
  2. 服务器主动异步通知 (Server Notifications) :你可以在 App Store Connect 中配置一个服务器端点(URL)。当交易状态发生变化(如购买成功、续订、退款、争议)时,苹果的服务器会主动向你的服务器发送 POST 请求 。这确保了即使因为网络问题客户端未能及时通知你的服务器,你仍然能通过苹果的回调获知交易状态并及时处理,这是防止漏单的最强有力手段
  3. 客户端本地验证与清晰的事务状态 :StoreKit 2 允许在客户端使用 Transaction.currentEntitlements 来获取用户当前有效的权益(已购买的商品),这有助于在应用启动时恢复购买和检查是否有未处理的交易。同时,清晰的事务状态(如 revoked)让你能更好地处理退款等场景。
  4. 服务端API与全局唯一事务ID :服务端可以通过 Get Transaction Info 等 API 查询任何交易的状态。结合全局唯一的 transactionId,你的服务器可以非常容易地实现幂等性(即对同一笔交易无论处理多少次,结果都一致),避免因重试导致的重复发货。

⚠️ 为什么仍不能保证100%不漏单?

尽管 StoreKit 2 非常强大,但在极端的分布式系统场景下,理论上仍存在极小概率的异常情况:

  • 苹果通知延迟或极端网络问题:虽然苹果服务器会重试发送通知,但在极罕见情况下,通知可能严重延迟或因你的服务器网络问题始终无法送达(尽管重试机制会降低此风险)。
  • 处理逻辑的健壮性 :你的服务器在接收和处理苹果通知、或者查询交易状态时,自身的业务逻辑必须足够健壮 。例如,要能够正确处理各种类型的通知(如 CONSUMPTION_REQUEST, DID_CHANGE_RENEWAL_PREF 等),并做好错误处理和日志记录。
  • "最后一道防线"的缺失:虽然不建议主要依赖,但 StoreKit 1 的"应用收据"包含了所有交易的历史记录,有时可作为最终对账的依据。而 StoreKit 2 更侧重于查询单笔交易状态,虽然也提供了查询历史交易的 API,但设计思路有所不同。

🛡️ 如何最大程度避免漏单(最佳实践)

  1. 务必配置并处理好 Server Notifications :这是最重要的一环。确保你的服务器端点能够正确验证苹果通知的签名,并处理所有相关的通知类型。
  2. 始终使用 appAccountToken:在发起购买时,传入一个与你自有订单系统关联的 UUID。
  3. 服务端做最终裁决 :所有发货逻辑都应放在服务端。客户端仅作为触发购买和查询状态的界面。服务端在接到客户端的交易ID或苹果的通知后,必须亲自调用苹果的 API 进行最终验证后再发货。
  4. 实现幂等逻辑 :基于 transactionIdappAccountToken,确保同一笔交易不会重复处理。
  5. 完善的日志和监控:记录所有验证请求、通知和发货流程的日志,并设置警报用于监控异常和失败情况。

总之,StoreKit 2 结合最佳实践,已经可以将漏单的风险降到非常非常低的水平 ,远超 StoreKit 1 的时代。你应该把重心放在正确实现和配置 StoreKit 2 提供的这些强大机制上,尤其是服务器通知appAccountToken 的使用上。

💡 如何兼容 StoreKit 1(旧版内购)与 StoreKit 2 ?

将现有的 StoreKit 1(旧版内购)与 StoreKit 2 进行兼容,关键在于根据系统版本动态选择 API,并确保服务端能处理两套凭证验证流程。下面是一个清晰的兼容方案,结合了 StoreKit 1 和 StoreKit 2 的特点。

特性/考虑点 StoreKit 1 (旧版) StoreKit 2 (新版) 兼容方案
​​最低系统要求​​ iOS 6.0+ iOS 15.0+ 根据 UIDevice.current.systemVersion 或 @available 检查进行条件编译和运行时 API 选择
​​编程语言​​ Objective-C, Swift Swift 使用 Swift,并通过 #available 条件编译隔离 API
​​核心支付方法​​ SKPaymentQueue 的 addPayment: Product 的 purchase() 封装一个统一的内购管理器,根据系统版本调用不同方法
​​订单关联标识​​ SKPayment 的 applicationUsername (可能不可靠) Product.PurchaseOption.appAccountToken(_:) (UUID, 可靠) ​​统一使用 UUID​​:在 StoreKit 1 中设置 applicationUsername,在 StoreKit 2 中设置 appAccountToken
​​交易监听与恢复​​ 遵循 SKPaymentTransactionObserver 协议,需手动管理交易队列 使用 Transaction.currentEntitlements 和 Transaction.updates 异步序列 监听并处理两套体系的事件流
​​凭证验证​​ 将整个 App Receipt (Base64 编码) 发送到服务器进行验证 获取 JWS 格式的 Transaction 或 signedPayload,客户端可本地验证,服务器需支持新老两种验证方式 ​​服务端需同时支持​​旧版收据验证 API 和新版 App Store Server API (JWS 验证)
​​服务器通知​​ 需处理 V1 版服务器通知 建议配置 V2 版服务器通知 (功能更强大) 建议服务器同时处理 V1 和 V2 通知,或根据业务需求选择配置

以下是实现兼容的关键步骤:

  1. 环境判断与API选择

在客户端,你需要根据系统版本决定使用哪套 API。这是兼容层的基础。

swift 复制代码
import StoreKit

class UnifiedIAPManager {
    
    static let shared = UnifiedIAPManager()
    private var sk1Available: Bool {
        // 检查 StoreKit 1 的类是否存在,判断其可用性
        return NSClassFromString("SKPaymentQueue") != nil
    }
    
    @available(iOS 15.0, *)
    private var sk2Available: Bool {
        // StoreKit 2 可用性检查,通常直接检查系统版本即可
        return true
    }
    
    func purchaseProduct(withId productId: String, forUser userId: String) {
        // 生成一个与订单关联的 UUID,这是关键!
        let orderUUID = UUID(uuidString: userId) // 或用其他方式生成与订单关联的UUID
        let appAccountToken = orderUUID
        
        if #available(iOS 15.0, *), sk2Available {
            // 使用 StoreKit 2 进行购买
            Task {
                do {
                    let products = try await Product.products(for: [productId])
                    guard let product = products.first else { return }
                    // 发起购买时传入 appAccountToken
                    let result = try await product.purchase(options: [.appAccountToken(appAccountToken)])
                    // 处理购买结果...
                } catch {
                    // 处理错误
                }
            }
        } else if sk1Available {
            // 使用 StoreKit 1 进行购买
            let request = SKProductsRequest(productIdentifiers: [productId])
            request.delegate = self
            request.start()
            // 在 productsRequest(_:didReceive:) 回调中,创建 SKPayment 并设置 applicationUsername
            let payment = SKMutablePayment(product: product)
            payment.applicationUsername = appAccountToken?.uuidString // 注意:StoreKit 1 中需要字符串
            SKPaymentQueue.default().add(payment)
        }
    }
}
  1. 统一订单关联标识

为了在所有 iOS 版本上可靠地关联订单,务必在两次购买流程中传入相同的 UUID

  • 在 StoreKit 2 中,使用 Product.PurchaseOption.appAccountToken(yourOrderUUID)
  • 在 StoreKit 1 中,将同一个 UUID 的字符串形式赋值给 SKPaymentapplicationUsername 属性 。

这个 UUID 应在购买前由你的服务器生成,并与用户的订单绑定。这样无论通过哪种方式购买,服务端都能凭此 UUID 找到对应订单进行发货。

  1. 处理交易更新和恢复购买

你需要同时处理两套监听体系:

swift 复制代码
class UnifiedIAPManager: NSObject, SKPaymentTransactionObserver {
    
    override init() {
        super.init()
        // 注册 StoreKit 1 的观察者
        SKPaymentQueue.default().add(self)
        
        // 如果系统支持,监听 StoreKit 2 的交易更新
        if #available(iOS 15.0, *) {
            Task(priority: .background) {
                for await update in Transaction.updates {
                    // 处理 StoreKit 2 的交易更新(如退款、争议)
                    await handleSK2TransactionUpdate(update)
                }
            }
        }
    }
    
    // MARK: - StoreKit 1 Transaction Observer
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            case .purchased:
                // 获取收据,发送到服务器验证
                verifyReceiptSK1(transaction: transaction)
                queue.finishTransaction(transaction)
            case .restored:
                // 处理恢复购买
                queue.finishTransaction(transaction)
            case .failed, .purchasing, .deferred:
                // 处理其他状态
                break
            @unknown default:
                break
            }
        }
    }
    
    // MARK: - StoreKit 2 Transaction Handling
    @available(iOS 15.0, *)
    private func handleSK2TransactionUpdate(_ update: Transaction) async {
        switch update {
        case .verified(let transaction):
            // 处理已验证的交易(如发货)
            await transaction.finish()
        case .unverified(let transaction, let error):
            // 处理未验证的交易(可能有风险)
            await transaction.finish()
        }
    }
    
    func restorePurchases() {
        if #available(iOS 15.0, *) {
            Task {
                // StoreKit 2 恢复购买
                try? await AppStore.sync() // 同步最新交易
                for await entitlement in Transaction.currentEntitlements {
                    // 遍历所有权益,恢复内容
                    if case .verified(let transaction) = entitlement {
                        // 根据 transaction.productID 恢复内容
                    }
                }
            }
        } else {
            // StoreKit 1 恢复购买
            SKPaymentQueue.default().restoreCompletedTransactions()
        }
    }
}
  1. 服务端验证兼容

服务端需要能够处理来自不同客户端的两种凭证 :

  1. 对于 StoreKit 1 :客户端会发送整个 App Receipt (Base64 编码)。服务端需调用苹果的 /verifyReceipt 端点进行验证。
  2. 对于 StoreKit 2 :客户端可能会发送 JWS 格式的 Transaction 信息transactionId。服务端应使用 App Store Server API (JWS 验证) 来查询和验证交易状态 。

服务端在收到验证请求后,应能通过客户端传来的 appAccountToken / applicationUsername (UUID) 准确关联到内部订单,这是实现可靠发货和防掉单的关键 。

  1. 测试与迁移策略
  2. 充分测试 :利用 Xcode 的 StoreKit Testing 功能 和沙盒环境,全面测试两种流程下的购买、恢复、异常处理等场景。
  3. 灰度发布:可以考虑先对部分用户或特定商品启用 StoreKit 2 路径,观察稳定性和数据对比。

⚠️ 注意事项

  • App Store Server Notifications :建议在 App Store Connect 中配置 V2 版服务器通知 ,以便苹果服务器在交易状态变化(如购买、退款、续订)时主动通知你的服务端,这能极大降低漏单风险。
  • 特定功能 :请注意,StoreKit 2 不支持 App Store 订阅推广(Promoted In-App Purchases)等特定功能 。如果你的应用依赖此类功能,则仍需保留 StoreKit 1 的相应实现。
  • 清晰抽象:良好的兼容性封装意味着业务代码不需要关心底层使用的是哪套 StoreKit API。

参考:

developer.apple.com/documentati...

juejin.cn/post/737394...

相关推荐
LuckySusu1 小时前
【js篇】JavaScript 原型修改 vs 重写:深入理解 constructor的指向问题
前端·javascript
LuckySusu1 小时前
【js篇】如何准确获取对象自身的属性?hasOwnProperty深度解析
前端·javascript
LuckySusu1 小时前
【js篇】深入理解 JavaScript 作用域与作用域链
前端·javascript
LuckySusu1 小时前
【js篇】call() 与 apply()深度对比
前端·javascript
LuckySusu1 小时前
【js篇】addEventListener()方法的参数和使用
前端·javascript
该用户已不存在1 小时前
6个值得收藏的.NET ORM 框架
前端·后端·.net
LuckySusu2 小时前
【js篇】深入理解 JavaScript 原型与原型链
前端·javascript
文心快码BaiduComate2 小时前
文心快码入选2025服贸会“数智影响力”先锋案例
前端·后端·程序员
云枫晖2 小时前
手写Promise-构造函数
前端·javascript