iOS 知识点 - IAP 是怎样的?

一、IAP 基本概念

  • 定义: In-App Purchase 是苹果提供的支付机制,允许用户在 App 内购买虚拟商品或订阅服务,所有数字内容和服务的交易必须通过 IAP 完成(苹果会抽成 15%~30%),否则会拒审。

  • 类型:

类型 描述 使用场景 特点
Consumable(消耗型) 用完即消失,可反复购买 金币、体力、道具 不可恢复,需自己记录消耗
Non-Consumable(非消耗型) 永久可用,购买一次即可 解锁关卡、去广告、付费功能 可恢复,支持跨设备恢复
Auto-Renewable Subscription(自动续订订阅) 周期性付费,自动续订 VIP 会员、内容订阅 苹果处理续订、退款、过期提醒
Non-Renewing Subscription(非续订订阅) 周期性付费,但需手动续订 课程、限时会员 不自动续订,需要自己管理过期

消耗型商品必须在验证成功后调 finishTransaction,否则 Apple 会认为你还没发货,下次启动继续提醒。

  • 四个核心名词:

    • 商品(Product):

      • 在 App Store Connect 后台配置的可购买项目。
      • 每个商品都有唯一的 productIdentifier,App 需要用这个 ID 去 Apple 查询商品的实时价格和货币信息,返回类型是 SPProduct(StoreKit 91) 或 Product(StoreKit 2)。
    • 订单(Order):

      • 自己服务器创建的记录。
      • 在调用 Apple 支付之前创建,拿到 order_id,用于后期的对账(钱对应哪个商品、给哪个用户、是购买还是赠送等等)。
      • Apple 不知道这个东西的存在,业务层概念。
    • 交易(Transaction):

      • Apple 侧产生的记录。
      • 在 Apple 的支付弹窗上确认付款后,Apple 会生成一笔交易。
      • 交易有多种状态:
      scss 复制代码
      purchasing(支付中) → purchased(已支付) → finished(已完成)
                        → failed(失败)
                        → deferred(等待家长审批)
                        → restored(恢复购买)
    • 收据(Receipt):

      • Apple 侧产生的付款凭证,证明用户确实付了钱。
      • 需要拿该凭证去服务器校验,服务器确认为真后才能发货,有两种格式:
        • StoreKit 1: 一个 Base64 编码的二进制文件(ASN.1),存在 App 沙盒里(Bundle.main.appStoreReceiptURL)。
        • StoreKit 2: 一个 JWS(JSON Web Signature)字符串,带有 Apple 的数字签名,更安全,不存本地,通过 Transaction API 获取。
  • 流程概述:

bash 复制代码
用户点击购买
  │
  ├─ ① App 用 productIdentifier 向 Apple 查询商品信息
  │     └─ Apple 返回 Product(价格、货币、描述)
  │
  ├─ ② App 向自己服务器创建订单,拿到 order_id
  │
  ├─ ③ 调用购买 API,Apple 弹出支付确认框
  │     └─ 用户输入密码 / Face ID / Touch ID
  │
  ├─ ④ Apple 返回交易结果(Transaction)
  │     ├─ purchased → 继续验证
  │     ├─ failed → 提示用户
  │     └─ deferred → 等待家长审批
  │
  ├─ ⑤ 拿 receipt/transaction 发给自己服务器验证
  │     └─ 服务器向 Apple 验证真伪(或本地验签 JWS)
  │     └─ 服务器确认后发货(加金币/开会员/解锁功能)
  │
  └─ ⑥ 调用 finishTransaction,告诉 Apple "我已发货"

二、StoreKit 1 与 StoreKit 2

维度 StoreKit 1(已废弃,但仍可用) StoreKit 2(推荐)
语言 Objective-C / Swift 均可 Swift only
异步模式 Delegate 回调 async/await
最低版本 iOS 3+ iOS 15+
交易监听 SKPaymentTransactionObserver Transaction.updates(AsyncSequence)
恢复购买 手动调 restoreCompletedTransactions() 自动可用,Transaction.currentEntitlements
收据验证 /verifyReceipt(服务端,已废弃) JWS 本地验签 / App Store Server API
订阅状态 自己解析 receipt 推算 Product.SubscriptionInfo.status 直接获取
退款处理 收不到通知 Transaction.revocationDate 有值 = 已退款
家庭共享 不支持 内置支持

2.1 StoreKit 1 核心类

objectivec 复制代码
SKProductsRequest          → 请求商品信息
  └─ SKProductsRequestDelegate  → 回调商品列表
       └─ SKProduct            → 单个商品(价格、标题、描述)

SKPayment                  → 支付请求对象
SKPaymentQueue             → 交易队列(单例)
  └─ SKPaymentTransactionObserver  → 交易状态回调
       └─ SKPaymentTransaction     → 单笔交易(状态、receipt)

SKReceiptRefreshRequest    → 强制刷新本地收据
  • StoreKit 1 典型代码流程:
swift 复制代码
// 1. 查询商品
let request = SKProductsRequest(productIdentifiers: ["com.app.coin100"])
request.delegate = self
request.start()

// 2. 收到商品信息
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
    let product = response.products.first!
    // 展示价格:product.localizedTitle, product.price
}

// 3. 发起购买
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)

// 4. 监听交易状态
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    for tx in transactions {
        switch tx.transactionState {
        case .purchased:
            // 验证收据 → 发货 → finish
            verifyAndDeliver(tx)
            queue.finishTransaction(tx)
        case .failed:
            queue.finishTransaction(tx)
        case .restored:
            queue.finishTransaction(tx)
        case .deferred, .purchasing:
        break
        }
    }
}

2.2 StoreKit 2 核心类

scss 复制代码
Product                         → 商品(静态方法 .products(for:) 查询)
Product.purchase()              → 发起购买,返回 PurchaseResult
Transaction                     → 交易(自带签名验证)
Transaction.updates             → AsyncSequence,监听新交易
Transaction.currentEntitlements → 当前有效权益(自动含恢复)
Transaction.finish()            → 确认已发货
Product.SubscriptionInfo        → 订阅状态(续订、过期、宽限期)
  • StoreKit 2 典型代码流程:
csharp 复制代码
// 1. 查询商品
let products = try await Product.products(for: ["com.app.coin100"])
let product = products.first!

// 2. 发起购买
let result = try await product.purchase()

switch result {
case .success(let verification):
    // 3. 验证交易
    switch verification {
    case .verified(let transaction):
        // 发货
        await deliverContent(transaction)
        // 4. 告诉 Apple 已发货
        await transaction.finish()
    case .unverified(_, let error):
        // 签名验证失败,可能被篡改
        handleError(error)
    }
case .userCancelled:
    break
case .pending:
    // 等待家长审批 / 支付确认
    break
}

// 5. App 启动时监听未完成的交易
Task {
    for await result in Transaction.updates {
        if case .verified(let transaction) = result {
            await deliverContent(transaction)
            await transaction.finish()
        }
    }
}

三、购买流程

3.1 正常购买流程

scss 复制代码
    App                     Apple                   自己服务器
     │                        │                        │
     │──Product.products()──> │                        │
     │<──返回商品信息─────────  │                        │
     │                        │                        │
     │──创建订单────────────────────────────────────────>│
     │<──返回 order_id──────────────────────────────────│
     │                        │                        │
     │──product.purchase()──> │                        │
     │    (用户确认支付)        │                        │
     │<──Transaction───────── │                        │
     │                        │                        │
     │──发送 transaction/receipt 验证─────────────────> │
     │                        │          │──验证签名/调 Apple API──>│
     │                        │          │<──确认有效──────────────│
     │<──验证通过,已发货─────────────────────────────────│
     │                        │                        │
     │──transaction.finish()─>│                        │
     │                        │                        │

3.2 异常场景处理

场景 现象 处理方式
支付成功但 App 崩溃/网络断开 没调 finish,下次启动 Apple 会重新推送交易 App 启动时监听 Transaction.updates,重新验证并发货
用户取消支付 result == .userCancelled 不做任何处理
家庭共享/家长审批 result == .pending 提示用户等待,后续通过 Transaction.updates 接收结果
重复购买(消耗型) 正常,消耗型可重复买 每次都走完整验证流程
重复购买(非消耗型) Apple 提示"你已购买过" 不会重复扣费,返回原始交易
退款 Apple 后台处理 服务端收到 S2S 通知 REFUND,或客户端检查 transaction.revocationDate
掉单(钱扣了但没收到交易) 极少见,通常是网络问题 服务端定期用 App Store Server API 查询用户交易历史

3.3 收据验证

客户端验证 服务端验证(推荐)
安全性 低,可被越狱设备绕过 高,服务器可信环境
实现复杂度 简单 需要后端配合
适用场景 个人开发者、低价值商品 商业应用、涉及虚拟货币

四、服务端通知(Server-to-Server Notifications)

Apple 会主动推送事件到你配置的服务器 URL(在 App Store Connect 中设置)。

4.1 主要通知类型

通知类型 含义
SUBSCRIBED 用户首次订阅
DID_RENEW 自动续订成功
EXPIRED 订阅过期
DID_FAIL_TO_RENEW 续订失败(信用卡过期等)
GRACE_PERIOD_EXPIRED 宽限期结束
REFUND 用户退款
REVOKE 家庭共享撤销
CONSUMPTION_REQUEST Apple 要求你提供消耗信息(用于退款裁决)
ONE_TIME_CHARGE 一次性购买通知(2025 新增)

4.2 通知格式

json 复制代码
{
  "signedPayload": "<JWS 字符串>"
}

解码 JWS 后得到:

json 复制代码
{
  "notificationType": "DID_RENEW",
  "subtype": "AUTO_RENEW",
  "data": {
    "signedTransactionInfo": "<JWS>",
    "signedRenewalInfo": "<JWS>"
  }
}

五、App Store Connect 配置

商品要在 App Store Connect 对应 App 的 App 内购买项目中配置并审核。

  • 沙盒测试:
    • App Store Connect → 用户和访问 → 沙盒测试员 → 添加测试 Apple ID
    • 手机 App Store 登入沙盒账号,测试环境会使用沙盒账号模拟支付。

六、常见的架构策略

6.1 项目分层

markdown 复制代码
View 层
  └─ 购买按钮、价格展示、订阅状态 UI

ViewModel / Manager 层
  └─ IAPManager(单例)
     ├─ 查询商品
     ├─ 发起购买
     ├─ 监听交易
     └─ 管理订阅状态

Service 层
  └─ IAPService(与服务器交互)
     ├─ 创建订单
     ├─ 验证收据
     └─ 查询购买记录

Apple 层
  └─ StoreKit 2 API

6.2 防掉单策略

markdown 复制代码
                    正常流程                         异常恢复
                  ┌──────────┐                   ┌──────────────┐
用户购买  ───────> │ 服务器验证 │──> 发货            │ App 启动      │
                  │ + finish │                   │ Transaction   │
                  └──────────┘                   │ .updates 推送 │
                                                 │ 未 finish 的  │
                                                 │ 交易          │──> 重新验证发货
                                                 └──────────────┘

服务端兜底:
  - 定期调 App Store Server API 查用户交易历史
  - 对比自己数据库,找出 Apple 有但自己没发货的交易
  - 补发

七、一些 2025 年新动向

  • StoreKit 1 已被标记为 Deprecated(WWDC 2024),虽然仍能用,但新项目应该用 StoreKit 2
  • /verifyReceipt 接口已废弃,改用 App Store Server API
  • S2S Notifications V1 已废弃,改用 V2
  • iOS 18.4+ 新增:
    • appTransactionID:每个 Apple 账户唯一标识
    • Offer Codes 扩展到消耗型/非消耗型商品(之前只支持订阅)
    • Advanced Commerce API:支持复杂的订阅附加组件
  • Xcode 16.2+ 必须升级,否则 iOS 18.2 上可能出现购买失败
相关推荐
tingshuo291715 小时前
D006 【模板】并查集
笔记
tingshuo29172 天前
S001 【模板】从前缀函数到KMP应用 字符串匹配 字符串周期
笔记
西岸行者7 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
starlaky7 天前
Django入门笔记
笔记·django
勇气要爆发7 天前
吴恩达《LangChain LLM 应用开发精读笔记》1-Introduction_介绍
笔记·langchain·吴恩达
悠哉悠哉愿意7 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
勇气要爆发7 天前
吴恩达《LangChain LLM 应用开发精读笔记》2-Models, Prompts and Parsers 模型、提示和解析器
android·笔记·langchain
qianshanxue117 天前
计算机操作的一些笔记标题
笔记
土拨鼠烧电路7 天前
笔记11:数据中台:不是数据仓库,是业务能力复用的引擎
数据仓库·笔记