App Store Connect审核拒绝原因:Guideline 3.1.1 – Business – Payments – In-App Purchase
App Store Connect审核拒绝原因:Guideline 3.1.1 – Business – Payments – In-App Purchase

App Store Connect审核拒绝原因:Guideline 3.1.1 – Business – Payments – In-App Purchase

在提审App Store Connect中,如果应用没有“恢复购买”功能,则可能会涉及Apple的App审核指南3.1.1条款的限制:

Guideline 3.1.1 - Business - Payments - In-App Purchase

The app offers in-app purchases that can be restored but does not include a "Restore Purchases" feature to allow users to restore the previously purchased in-app purchases.

翻译为:

指南 3.1.1 - 业务 - 付款 - 应用内购买
该应用提供可恢复的应用内购买,但不包含“恢复购买”功能,以允许用户恢复之前购买的应用内购买。

因此,应用需要提供“恢复购买”功能,以恢复内购商品。

处理“恢复内购”按钮

内购UI

创建一个按钮来触发恢复购买流程:

// 恢复内购按钮
Button(action: {
    // 调用恢复内购代码
    Task {
        await iapManager.restorePurchases()
        
        // ✅ 主线程更新 loadPurchased 和 endOfWait
        DispatchQueue.main.async {
            iapManager.loadPurchased = false // 结束加载动画
        }
        
        try? await Task.sleep(nanoseconds: 20_000_000_000) // 延迟 20 秒
        DispatchQueue.main.async {
            endOfWait = true // ✅ 主线程更新 endOfWait
        }
    }
    DispatchQueue.main.async {
        iapManager.loadPurchased = true // ✅ 显示加载动画
    }
}, label: {
    Text("Restore purchases")
        .font(.footnote)
        .foregroundColor(.gray)
})

需要注意的是,iapManager.loadPurchased是我的加载动画,当加载超过20秒会显示结束按钮,因此,实际的代码为:

// 恢复内购按钮
Button(action: {
    // 调用恢复内购代码
    Task {
        await iapManager.restorePurchases()
    }
}, label: {
    Text("Restore purchases")
        .font(.footnote)
        .foregroundColor(.gray)
})

通过点击“恢复内购”按钮,使用Task调用restorePurchases方法,iapManager是一个管理内购的类,也可以将restorePurchases放在当前视图。

// 恢复内购按钮
Button(action: {
    // 调用恢复内购代码
    Task {
        await restorePurchases()
    }
}, label: {
    Text("Restore purchases")
        .font(.footnote)
        .foregroundColor(.gray)
})

内购代码

// 调用恢复购买的函数
func restorePurchases() async {
    let entitlements = Transaction.currentEntitlements
    var hasTransactions = false
    print("开始等待 Transaction.currentEntitlements")
    for await result in entitlements {
        hasTransactions = true
        print("发现交易:\(result)")
        switch result {
        case .verified(let transaction):
            print("处理已验证的交易")
            // 处理已验证的交易
            await handleVerifiedTransaction(transaction)
        case .unverified(_, let error):
            print("交易未验证,错误:\(error.localizedDescription)")
        }
    }
    
    if !hasTransactions {
        print("没有找到任何有效的交易,可能是用户没有购买过任何项目")
    }
}

// 处理已验证的交易
func handleVerifiedTransaction(_ transaction: Transaction) async {
    print("transaction:\(transaction)")
    switch transaction.productType {
    case .nonConsumable, .autoRenewable, .nonRenewable:
        print("已恢复商品:\(transaction.productID)")
        savePurchasedState(for: transaction.productID)	// 恢复内购的方法
    default:
        print("其他类型商品无需恢复")
    }
}
代码解析

1、let entitlements = Transaction.currentEntitlements

当调用restorePurchases方法后,会调用Transaction.currentEntitlements异步序列,返回当前有效的交易(entitlements)。

这个序列会返回所有未过期的订阅、非消耗型商品、和非续期订阅的交易信息。

但如果没有交易信息,则不会触发任何操作

2、for await result in entitlements

这里的 for await 是 Swift 的 异步遍历,它会逐个获取 Transaction 结果。

如果有有效交易,它会进入 switch result 处理。

3、switch result逻辑解析

1)case .verified(let transaction)

验证成功的交易

交易通过 StoreKit 验证(没有被篡改),可以安全地进行恢复。

进入 handleVerifiedTransaction(transaction) 处理已恢复的商品。

2)case .unverified(_, let error)

未验证的交易

交易未通过 StoreKit 验证,可能存在安全风险。

error.localizedDescription 会显示未验证交易的具体错误原因。

4、hasTransactions 标志

var hasTransactions = false

如果 Transaction.currentEntitlements 发现有可恢复的交易,hasTransactions 会被设置为 true。

如果没有找到任何有效的交易,会打印:

print("没有找到任何有效的交易,可能是用户没有购买过任何项目")

5、交易处理结束的逻辑

如果 hasTransactions 仍然是 false,说明 没有有效的交易需要恢复,会提示:

"没有找到任何有效的交易,可能是用户没有购买过任何项目"

6、handleVerifiedTransaction(_ transaction:) 方法

transaction.productType 会返回购买的商品类型,常见的包括:

.nonConsumable:非消耗型商品(一次性购买,永久有效)。

.autoRenewable:自动续期订阅。

.nonRenewable:非续期订阅(例如:7 天会员)。

7、商品恢复逻辑

如果商品是非消耗型、自动续期订阅或非续期订阅:

savePurchasedState(for: transaction.productID)

savePurchasedState() 是自定义函数,用于存储商品已恢复的状态(通常保存在 UserDefaults或本地数据库中)。

8、savePurchasedState(for:) 方法

func savePurchasedState(for productID: String) {
    // 保存购买状态(例如:存储在 UserDefaults 或 Keychain 中)
    UserDefaults.standard.set(true, forKey: productID)
    print("已恢复并存储商品状态:\(productID)")
}

常见问题和注意事项

只需要对非消耗型商品和订阅恢复购买,消耗型商品(如游戏内货币)无法恢复。

确保 restorePurchases() 逻辑中包含 Transaction.currentEntitlements 以获取所有当前有效的交易。

在 App Store Connect 中配置正确的 App 内购买项目(IAP) 并启用 StoreKit 权限。

总结

这篇文章实际上是《iOS通过StoreKit 2实现应用内购》文章的扩展,不理解的地方可能需要到前文进行查看。

最后的实现效果为,当没有内购时,点击“恢复内购”按钮,会输出“没有找到任何有效的交易,可能是用户没有购买过任何项目“,不会有其他的操作。

如果已经完成内购,点击“恢复内购“按钮后,就会调用savePurchasedState(for: transaction.productID)方法,恢复具体的内购商品。

参考文章

1、App 审核指南:https://developer.apple.com/cn/app-store/review/guidelines/

2、iOS通过StoreKit 2实现应用内购:https://fangjunyu.com/2024/05/30/ios%e5%ae%9e%e7%8e%b0%e5%ba%94%e7%94%a8%e5%86%85%e8%b4%ad/

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注