苹果开发者生态
Apple App 内购项目
苹果审核注意事项
苹果审核被拒后如何解决?
App Store Connect 后台管理
苹果开发者账号
TestFlight 测试
本文档使用 MrDoc 发布
-
+
首页
Apple App 内购项目
App 内购买项目(In-app purchase)简称 IAP(后文统一使用 IAP 代替),在所有 Apple 平台上,你都可以利用 IAP,直接在 App 内提供额外的内容和功能,包括数字商品、订阅和增值内容等。根据 [App Store 审核指南 3.1.1 App 内购买项目](https://developer.apple.com/cn/app-store/review/guidelines/#in-app-purcha): 如果你想要在 App 内解锁特性或功能 (解锁方式有:订阅、游戏内货币、游戏关卡、优质内容的访问权限或解锁完整版等),则必须使用 App 内购买项目。App 不得使用自身机制来解锁内容或功能,如许可证密钥、增强现实标记、二维码、加密货币和加密货币钱包等。除非符合 [3.1.3(a) ](https://developer.apple.com/cn/app-store/review/guidelines/#reader-apps)中规定的情况,否则 App 及其元数据不得包含按钮、外部链接或其他触发方式,引导用户使用 App 内购买项目以外的购买机制。 在 App Store 中,想让自己的 App 实现盈利,除了实物购买交易,我们的唯一选择就是加入 IAP。当然,项目中集成了 App 内购订阅,苹果的审核也会更加严格,稍有疏忽就可能会导致 App 被拒绝。本篇内容会从头介绍 App 内购买项目相关的知识,涉及到内购订阅方方面面的内容。在 App 中集成 NTIAPTool 工具来快速了解 StoreKit 并进行测试,关于内购会遇到大多数的坑,我们也有一份注意事项来帮你排疑解惑。 ## App 内购买项目 App 内购买项目是指你在 App 内购买的额外内容或订阅。通常情况下,我们熟悉使用微信与支付宝在第三方 App 上进行付款购买商品,Apple 提供的 IAP 也是类似的购买服务,两者主要的不同在于 Apple 内购主要服务于虚拟商品。我们开发的 App 想要上架 App Store,其中集成了增值服务,就必须准守该审核指中的该条款。 ### IAP 介绍 IAP 分为四种类型,在我们的 App 可提供多种类型的内购买项目。 - 消耗型项目:提供不同类型的消耗型项目,例如游戏中用来推动进程的生命或宝石,约会 App 中用来提升个人资料曝光度的升级,或者社交媒体 App 中的创作者数字提示。消耗型的 App 内购买项目在使用之后即失效,并可再次购买。(主要表现为大热游戏中的 648 点券,直播平台打赏礼物或微信豆充值。因为 Apple 会收取 30% 手续费,在 Android 平台获得相同的服务要比 iOS 平台多 30 % 的价钱) - 非消耗型项目:提供非消耗型项目,解锁更多进阶功能。这些功能只需购买一次,并且不会过期。例如,照片 App 中的额外滤镜、插图 App 中的额外画笔或游戏中的皮肤(类似 App 中提供的永久会员 VIP,一次购买解锁,卸载 App 重新安装后可以通过“恢复购买”来重新获取服务) - 自动续期订阅:提供对 App 中内容、服务或进阶功能的持续访问权限。此类订阅会自动续期,除非用户选择取消。常见用例包括提供媒体或内容库 (例如视频、音乐或文章) 访问权限、软件即服务 (例如云存储、效率或图形与设计) 以及教育等等。(如国内优爱腾提供的视频会员包月包年,购买一次,到期后会自动扣款续期。现在主流 App 都提供续订服务,续订对于 App 保持收入非常重要) - 非续期订阅:对相关服务或内容提供有时限性的访问权限 (例如游戏中内容的季度订阅)。(还是优爱腾的视频会员服务,不同于续订只需购买一次,购买一个月的会员,非续订到期需要用户再次购买) 苹果用户使用 Apple 提供的 IAP 服务,可以获得一致且安全的体验: - 使用与 Apple ID 关联的付款方式,以 45 种货币快速付款,支持近 200 种付款方式 (包括 Apple Pay、信用卡或借记卡、商店积分、地区特定付款方式等 — 信息皆会被安全地储存)。 (App Store 国区用户可以直接绑定微信或者支付宝来进行付款) - 在 App 支持的所有设备上访问他们购买的内容,并在新设备上恢复购买项目。 (多设备如:iPhone、iPad、Mac、Apple Watch等) - 使用“报告问题”就他们所购买的内容获取协助或请求退款。 - 通过“家人共享”共享符合条件的购买项目。 在他们的 Apple 设备上查看购买历史记录。 - 在一处集中管理他们的所有订阅(App Store 账户中的订阅列表)。 当我们的 App 内购买项目发布后,就可以在 App Store 上以及 App 内推广它们,展示它们的价值。此外,利用营销指导、工具、促销优惠等帮助提升我们的 App、App 内购买项目和订阅的曝光率,详情查看[推广您的 App](https://developer.apple.com/cn/app-store/promote/)。 ### 自动续期订阅 自动续期订阅让用户能持续地访问 App 中的内容、服务或高级功能。这些订阅会在其持续时间结束时自动续期,直至用户选择取消。例如,你可以按月订阅 Apple Music。订阅包括你在 App 中注册的服务,例如视频流媒体服务或音乐流媒体服务。订阅在 iOS、iPadOS、macOS、watchOS 和 Apple tvOS 上皆可提供。 除非你取消订阅,否则大多数订阅会自动续订。对于某些 App 和服务,你可以选取订阅续订的频率。例如,这些 App 和服务可能会为你提供每周、每月、每季度或每年的订阅选项。  自动续期订阅的净收入结构和 App Store 上的其他商业模式不同,满一年后可获 85% 净收入。在订阅者第一年服务的每个结算周期,你会收到订阅价格的 70% (减去适用税款)。订阅者累积满一年付费服务后,你的收入将增加到订阅价格的 85% (减去适用税款)。 累积方式如下: - 所有 Apple 平台的自动续期订阅皆符合资格 - 付费服务天数包括所有订阅优惠类型 (推介促销优惠、促销优惠和优惠代码),以及付费定价选项 (随用随付、提前支付) - 免费试用和续订扩展不包括在付费服务天数内 - 每个订阅群组的付费服务天数可以有所不同 - 同一订阅群组内的订阅升级、降级或跨级不影响一年付费服务的天数累积 - 如果你当前注册的是 App Store Small Business Program,你会在每个结算周期收到订阅价格的 85% (减去适用税款),无论订阅是否已累积满一年付费服务。(优惠详细查看[小型业务和个人开发者](https://developer.apple.com/cn/app-store/small-business-program/)) 让用户能够在他们对订阅价值最感兴趣的时刻试用订阅,可以加强用户订阅的意愿,可以通过多种方法提供订阅体验预览: - 在免费试用的购买流程中,清楚地指明免费试用期限,以及免费试用结束后继续使用需要支付的价格 - 直接在 App Store 上[推广 App 内购买项目](https://developer.apple.com/cn/app-store/promoting-in-app-purchases/),这样用户就能找到你的订阅或推介促销优惠,甚至在下载你的 App 之前就开始购买这些项目 - 允许用户在特定时限内免费获取或以折扣价购买自动续期订阅,以此来扩大和留存用户群。优惠期结束后,订阅会以标准价格自动续订,除非订阅者取消订阅或关闭自动续订功能 - 利用线上渠道或线下渠道来分发优惠代码,以优惠价格或限时免费形式来提供订阅,可以帮助你获取、留存和赢回订阅者 - 在特定时限内向现有或之前的订阅者提供免费或折扣订阅。灵活地创建独特的促销活动,以增加和留存顾客;还能帮你赢回已取消订阅的顾客;或以特价促进订阅的升级率 ### 提供 App 内购项目 在创建 App 内购买项目并在 App 中提供之前,你需要签署付费 App 协议,并在 App Store Connect 中设置好你的[银行和税务信息](https://help.apple.com/app-store-connect/?lang=zh-cn#/devb6df5ee51)。 在 App Store Connect 中配置 App 内购买项目,为其添加产品名称、描述、价格和销售范围等详细信息。添加本地化信息,确保多个地区的用户能以其首选语言获得顺畅的购买体验。在 App Store Connect 中的操作步骤:点按“我的 App”,然后选择 App。 在侧边栏的“功能”下方,点按“App 内购买项目”。 点按添加按钮(+),创建新的 App 内购买项目。详情请查看 [App 内购买项目配置流程](https://help.apple.com/app-store-connect/?lang=zh-cn#/devb57be10e7)。  创建好 App 内购买项目后,再添加审核信息,包括截屏与审核备注,这有助于 Apple 审核你的 App 内购买项目。此信息仅用于 Apple 的审核,不会在 App Store 中显示。  在开发早期,你可以使用 Xcode 来模拟和测试 App 内购买项目,确保你的 App 和服务器能够正确处理常见的购买情景,如订阅优惠、中断的购买或退款。在 App Store Connect 中配置 App 内购买项目后,可以在沙盒环境中或使用 Xcode 中的 StoreKit 测试功能,使用真实产品信息进行测试。 [沙盒测试](https://help.apple.com/app-store-connect/?lang=zh-cn#/dev8b997bee1)让你能够在不产生 StoreKit 交易的情况下测试 App 内购买项目。沙盒是一个使用 App Store 基础架构但不处理实际付款的测试环境。此环境下,系统会默认付款已成功处理并返回交易。 你可以测试 App Store Connect 中显示的所有 App 内购买项目。请注意,你无需上架 App 内购买项目即可对其进行测试。如果你将 App 内购买项目设置为“准许销售”(订阅项目设置为“上架”),该项目会在生产环境中发布。在沙盒测试中测试订阅项目,系统会在计费重试期间尝试恢复订阅至少 6 次,续费间隔时间为几分钟到几小时不等,具体查看[订阅项目续期速率](https://help.apple.com/app-store-connect/?lang=zh-cn#/dev7e89e149d)。 从 App Store 购买的某些项目可能符合退款条件,你可以使用任何带有网页浏览器的设备来[申请退款](https://support.apple.com/zh-cn/HT204084)。在项目中集成 [Sign in with Apple 登录](https://www.yuque.com/eternaljust/rpmt31/bxmc3d#khpAl),App 注销用户账号时,对于正在订阅自动续费的用户,提供打开 App Store 订阅管理购买项目,具体查看下面`NTIAPTool`中的 `nt_openSubscriptions`方法。 ## NTIAPTool 工具 `NTIAPTool`是完全独立的工具类,只包含`NTIAPTool.h`和`NTIAPTool.m`两个文件,仅依赖 Apple 官方的内购库`StoreKit`,内部支持 keychain 持久化缓存,方法注释清晰,日志打印详细。推荐采用 CocoaPods 自动集成的方式,方便管理升级版本,也可直接访问 [git 项目地址](http://coding.nineton.cn/tzqiang/NTIAPTool),下载并手动拖入源文件到项目中。 ``` pod 'NTIAPTool', '1.0.1' ``` ### 内购订阅流程图 为了方便理解`NTIAPTool`工具类,和天气后端同学一起整理了内购订阅相关的流程图:  ### Demo 使用参考 `NTIAPTool` 调用方法与相关的属性回调覆盖了流程图的方方面面,已正常上线项目并通过了验证。下面是一段具体的 Demo 代码,注释内容可根据项目组实际的业务来处理: ```objectivec - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [[NTIAPTool shareInstance] nt_addIAPObserver]; [NTIAPTool shareInstance].failureBlock = ^(NTIAPTransactionError errorCode, NSString * _Nonnull errorMessage) { if (errorCode == NTIAPTransactionErrorTransactionNotFinished || errorCode == NTIAPTransactionErrorRestoreTransactionFailed) { // AnalysisTool.event("订单交易失败", attributes: ["type" : message]) } else { // AnalysisTool.event("直接购买失败", attributes: ["type" : message]) } // HUDTool.showHUD(message) }; [NTIAPTool shareInstance].verifyTransactionReceiptHandler = ^(NTIAPReceiptParams * _Nonnull params, SKPaymentTransaction * _Nonnull transaction) { if (params.receipt.length == 0) { // AnalysisTool.event("订单交易失败", attributes: ["type" : "receipt 票据为空"]) } [self checkReceiptToServerWith:[self mapParams:params] isKeychain:NO handeler:^(BOOL result) { if (result) { [[NTIAPTool shareInstance] nt_finishTransaction:transaction]; } }]; }; [NTIAPTool shareInstance].verifyKeychainReceiptHandler = ^(NTIAPReceiptParams * _Nonnull params) { if (params.receipt.length == 0) { // AnalysisTool.event("订单交易失败", attributes: ["type" : "receipt 票据为空"]) } [self checkReceiptToServerWith:[self mapParams:params] isKeychain:YES handeler:^(BOOL result) { if (result) { [self deleteKeychain]; } }]; }; // Override point for customization after application launch. return YES; } - (NSDictionary *)mapParams:(NTIAPReceiptParams *)params { NSMutableDictionary *paramString = [NSMutableDictionary dictionary]; paramString[@"receipt"] = params.receipt; paramString[@"transactionId"] = params.transactionId; paramString[@"productId"] = params.productId; return paramString; } /// 恢复购买 - (void)restoreIAP { // AnalysisTool.event(@"恢复购买") [[NTIAPTool shareInstance] nt_restoreWithHUDHandler:^{ // HUDTool.alwaysShow("正在为您恢复购买...") } completedTransactionsHUDHandler:^{ // HUDTool.showHUD("已处理完所有可恢复交易") }]; } /// 购买订阅 - (void)buyWithProductId:(NSString *)productId { // AnalysisTool.event(@"发起购买订阅") // if ([MobClick isJailbroken]) { // AnalysisTool.event("直接购买失败", attributes: ["type" : "越狱设备不支持购买"]) // HUDTool.showHUD("越狱设备不支持购买") // return // } [[NTIAPTool shareInstance] nt_buyWithProductId:productId hudHandler:^{ // HUDTool.alwaysShow("正在购买...") }]; } /// 持久化的票据进行恢复校验 - (void)checkKeychainReceipt { if ([[NTIAPTool shareInstance] nt_hasKeychainReceipt]) { // keychainRetryCount = 0 [[NTIAPTool shareInstance] nt_verifyKeychainReceiptHUDHandler:^{ // HUDTool.alwaysShow("正在为您恢复处理异常订阅订单...") }]; } } /// 删除持久化的交易数据 - (void)deleteKeychain { [[NTIAPTool shareInstance] nt_deleteKeychainReceipt]; } /// 与服务器校验票据信息 - (void)checkReceiptToServerWith:(NSDictionary *)params isKeychain:(BOOL)isKeychain handeler:(void (^)(BOOL result))handeler { // [NetworkTool post:@"/applePay/receipt" // params:params // successed:^(id object) { // HUDTool.showHUDSuccess("订阅成功!") // AnalysisTool.event("订阅成功") // handeler(true); // // 网络错误重试 // networrkRetryCount = 0; // // keychain 重试 // keychainRetryCount = 0; // } failed:^(NSError * error) { // handeler(false); // [self retryReceiptToServerWith:params isKeychain:isKeychain handeler:^(BOOL result) { // handeler(result); // }]; // }]; } /// 弹窗提示用户重试 - (void)retryReceiptToServerWith:(NSDictionary *)params isKeychain:(BOOL)isKeychain handeler:(void (^)(BOOL result))handeler { // NSInteger count = networrkRetryCount; // if (isKeychain) { // count = keychainRetryCount; // } // // 多次尝试 // if (count < 3) { // if (isKeychain) { // keychainRetryCount += 1; // AnalysisTool.event("内购 keychain 重试", attributes: ["count" : keychainRetryCount]) // } else { // networrkRetryCount += 1; // AnalysisTool.event("内购网络重试", attributes: ["count" : networrkRetryCount]) // } // // UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:@"错误提示!" message:@"购买订单验证异常,是否要重试?" preferredStyle:UIAlertControllerStyleAlert]; // UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { // HUDTool.hideHUD() // }]; // UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"重试" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { // [self checkReceiptToServerWith:params isKeychain:isKeychain handeler:^(BOOL result) { // handeler(result); // }]; // }]; // } else { // // 尝试 n 次后,上传收集日志成功后,再删除 keychain 缓存的交易数据 // [NetworkTool post:@"/applePay/report" // params:params // successed:^(id object) { // handeler(false) // [self deleteKeychain]; // } failed:^(NSError * error) { // handeler(false); // }]; // } } /// 跳转 App Store 订阅管理 - (void)openSubscriptions { [[NTIAPTool shareInstance] nt_openSubscriptions]; } ``` ### NTIAPTool 错误码 使用`NTIAPTool`过程中,需要关注错误回调`void (^failureBlock)(NTIAPTransactionError errorCode, NSString *errorMessage)`返回的错误码与错误描述,进行埋点统计上报。 ```objectivec typedef NS_ENUM(NSInteger, NTIAPTransactionError) { /// 未知错误 NTIAPTransactionErrorUnknown = 0, /// 产品 ID 为空 NTIAPTransactionErrorProductIdEmpty, /// 设备不支持购买 NTIAPTransactionErrorCannotMakePayments, /// 有交易未完成 NTIAPTransactionErrorTransactionNotFinished, /// 获取产品信息失败 NTIAPTransactionErrorGetProductFailed, /// 交易队列为空 NTIAPTransactionErrorTransactionsEmpty, /// 交易失败:SKErrorCode.错误描述,例如(2.用户取消了付款请求) NTIAPTransactionErrorTransactionFailed, /// 恢复购买失败:SKErrorCode.错误描述,例如(0.未知错误) NTIAPTransactionErrorRestoreTransactionFailed }; ``` #### 错误提示:当前设备不支持购买 交易发起前会判断当前设备环境是否支持购买,如果手机不支持 Apple Pay 购买,比如没有绑定支付卡,硬件可能不支持支付功能,或者受到家长控制的限制等原因,则会提示当前用户“当前设备不支持购买”并结束交易流程。 具体判断代码如下: ```objectivec if (![SKPaymentQueue canMakePayments]) { NSString *errorMessage = @"当前设备不支持购买"; NSLog(@"-IAP- 交易失败:%@", errorMessage); BLOCK(self.failureBlock, NTIAPTransactionErrorCannotMakePayments, errorMessage); return; } ``` ```canMakePayments``` 官方文档说明:   ### SKErrorCode 错误码 特别说明,`NTIAPTransactionErrorTransactionFailed`与`NTIAPTransactionErrorRestoreTransactionFailed`是苹果内购交易过程中(包括恢复购买)发生的错误类型`SKError`,错误描述`errorMessage`如“2.用户取消了付款请求”。其中 2 是错误码`SKErrorPaymentCancelled`,“用户取消了付款请求” 是其定义的中文描述,项目组也可只用 “2.” 来获取错误码,再根据 Apple [StoreKit 官方文档](https://developer.apple.com/documentation/storekit/skerror/code)的定义`SKErrorCode`来描述错误。 其他的如“2.无法连接iTunes Store”,“无法连接iTunes Store” 是系统返回的本地化错误描述,优先取该值,若空再取`SKErrorCode`的中文描述。 ```objectivec // error codes for the SKErrorDomain typedef NS_ENUM(NSInteger,SKErrorCode) { SKErrorUnknown, SKErrorClientInvalid, // client is not allowed to issue the request, etc. SKErrorPaymentCancelled, // user cancelled the request, etc. SKErrorPaymentInvalid, // purchase identifier was invalid, etc. SKErrorPaymentNotAllowed, // this device is not allowed to make the payment SKErrorStoreProductNotAvailable API_AVAILABLE(ios(3.0), macos(10.15), watchos(6.2)), // Product is not available in the current storefront SKErrorCloudServicePermissionDenied API_AVAILABLE(ios(9.3), tvos(9.3), watchos(6.2), macos(11.0)), // user has not allowed access to cloud service information SKErrorCloudServiceNetworkConnectionFailed API_AVAILABLE(ios(9.3), tvos(9.3), watchos(6.2), macos(11.0)), // the device could not connect to the nework SKErrorCloudServiceRevoked API_AVAILABLE(ios(10.3), tvos(10.3), watchos(6.2), macos(11.0)), // user has revoked permission to use this cloud service SKErrorPrivacyAcknowledgementRequired API_AVAILABLE(ios(12.2), tvos(12.2), macos(10.14.4), watchos(6.2)), // The user needs to acknowledge Apple's privacy policy SKErrorUnauthorizedRequestData API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2)), // The app is attempting to use SKPayment's requestData property, but does not have the appropriate entitlement SKErrorInvalidOfferIdentifier API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2)), // The specified subscription offer identifier is not valid SKErrorInvalidSignature API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2)), // The cryptographic signature provided is not valid SKErrorMissingOfferParams API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2)), // One or more parameters from SKPaymentDiscount is missing SKErrorInvalidOfferPrice API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2)), // The price of the selected offer is not valid (e.g. lower than the current base subscription price) SKErrorOverlayCancelled API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2)), SKErrorOverlayInvalidConfiguration API_AVAILABLE(ios(14.0)) API_UNAVAILABLE(macos, watchos) __TVOS_PROHIBITED, SKErrorOverlayTimeout API_AVAILABLE(ios(14.0)) API_UNAVAILABLE(macos, watchos) __TVOS_PROHIBITED, SKErrorIneligibleForOffer API_AVAILABLE(ios(14.0), macos(11.0), watchos(7.0)), // User is not eligible for the subscription offer SKErrorUnsupportedPlatform API_AVAILABLE(ios(14.0), macos(11.0), watchos(7.0)) __TVOS_PROHIBITED, SKErrorOverlayPresentedInBackgroundScene API_AVAILABLE(ios(14.5)) API_UNAVAILABLE(macos, watchos) __TVOS_PROHIBITED // Client tried to present an SKOverlay in UIWindowScene not in the foreground } API_AVAILABLE(ios(3.0), macos(10.7), watchos(6.2)); ``` 上报交易失败的错误描述,错误码 + 系统返回本地化描述,以下统计截图来源天气项目:  ### 重试机制 使用`NTIAPTool`进行内购时,建议加入重试机制,包括网络错误重试与持久化缓存重试。网络错误重试可在第一次发起内购交易票据校验失败时,弹窗提示用户重新校验,重试间隔时间递增,已排除用户短时间网络异常无法访问网络。另外,keychain 钥匙串保存 receipt 交易票据信息也十分重要。交易队列信息状态更新为交易完成时,此时`NTIAPTool`会持久化关键数据:票据 receipt、交易 transactionId、 产品 productId 到 keychain 中,只有当票据信息与服务器完成校验后,手动调用`nt_finishTransaction:`来结束当前的交易。如果票据校验失败,网络重试几次也失败,提示用户重启 App 将重新尝试进行票据校验,以此来恢复用户的购买交易,防止内购异常情况下的丢单漏单。 ## 注意事项 ### 创建内购项目 想要创建新的 App 内购买项目,必须先得接受 App Store Connect 的《付费应用程序协议》,具体的步骤请参阅[协议、税务和银行业务概述](https://help.apple.com/app-store-connect/#/devb6df5ee51)。创建 App 内购买项目需要你当前账户的职能权限为:帐户持有人、管理、App 管理、开发者或营销,编辑与提交 App 内购买项目需要职能为:帐户持有人、管理、App 管理。创建产品 ID 需要正确填写元数据,包括选择 App 内购买项目类型,设置正确的价格,完善审核信息截图。每次新创建的产品 ID 都需要审核之后才能上线正式使用(沙盒测试期间不需要审核),此时必须确保 App 审核后的发布方式为手动,防止 App 新版通过审核,然后创建的内购项目还没有通过审核的情况,造成线上的内购项目无效不可以,导致用户不能进行内购付款。 ### 分组管理 同一个订阅分组内的不同套餐可以升级降级,比如周、月、季和年这种不同等级的。永久会员属于非消耗型项目,不支持与订阅组套餐之间的切换。 针对项目中出现过用户已经购买了订阅类型会员,后面不小心又购买了永久会员,后续如何进行处理的问题,可以有两种处理方案: 1. 会员购买套餐选择购买前,判断当前用户的会员类型,如果已是月年的连续订阅用户,再购买永久会员时提示用户当前已存在签约信息。如需购买永久会员,提示用户先自己手动取消已购买的订阅项目,然后才能支持购买永久会员。 2. 已经购买了订阅类型会员,后面不小心又购买了永久会员,前端暂时处理不了。只能通过客服联系用户手动去 App Store 取消订阅,后端同时处理好用户取消订阅后接受到苹果那边的退订通知逻辑。 永久会员用户也不用担心退订已购订阅影响会员时长的问题,有问题的话可以通过恢复内购来重新获得会员权益。 对于创建内购项目为订阅类型,在配置订阅需要关注做好分组管理,其中涉及以下几个方面: - 订阅群组:一个订阅群组由多个不同等级和时限的订阅产品组成。对于每个订阅群组,用户只能订阅其中的一个产品。 - 自动续期订阅:在一定时限内为用户提供动态内容的产品。此类订阅会自动续期,直至用户取消订阅。订阅的时限和价格均由你在 App Store Connect 中设置。 - 订阅等级:一个订阅群组内的排序机制,为用户提供不同等级的订阅产品,用户可以按需升级、降级和同级转换。通过设置订阅产品的等级,你可以为同一款服务提供差异化的访问权限(例如基础版、高级版)。 - 订阅时限:订阅每次续期的间隔时长,也是订阅者每次付费的间隔时长。订阅时限可为 1 周、1 个月、2 个月、3 个月、6 个月或 1 年。 ### TestFlight 新创建没有通过审核的内购项目都属于测试产品,App 可以在 Debug 环境下进行沙盒账户测试购买订阅流程。当 App 上传到 App Store Connect 后台通过处理后,通过 TestFlight 进行安装测试的 App 比较特殊,此时还是属于沙盒测试环境,可调用`NTIAPTool`中的`nt_getIsSandbox`方法进行判断: ```objectivec - (BOOL)nt_getIsSandbox { BOOL isSandbox = NO; #if DEBUG isSandbox = YES; #else NSString *lastPathComponent = [NSBundle mainBundle].appStoreReceiptURL.lastPathComponent; if ([lastPathComponent containsString:@"sandbox"]) { // TestFlight isSandbox = YES; } else { isSandbox = NO; } #endif return isSandbox; } ``` ### 订阅相关 必须在`didFinishLaunchingWithOptions`启动时调用`nt_addIAPObserver`加入观察监听(沙盒测试阶段续订会重复几次,属于正常现象,具体查看[订阅续费间隔时间](https://help.apple.com/app-store-connect/#/dev7e89e149d)),防止内购订阅订单漏单。只有交易事务被正常结束(主动调用`[[SKPaymentQueue defaultQueue] finishTransaction:transaction];`完结购买交易)本次支付行为才算完成, 或者自动订阅续费(续费都 Apple 自动完成的,一般在要过期的前 24 小时开始,苹果会尝试扣费, 扣费成功的话会在 App 下次启动的时候主动推送给 App。),在每次 App 启动时,通过调用`nt_addIAPObserver`就会触发回调之前所有未完结的事务`paymentQueue:updatedTransactions:`。 想要避免内购订阅丢单漏单,特别关注四点即可: - 在`didFinishLaunchingWithOptions`启动时调`nt_addIAPObserver`注册支付交易队列观察者 - 只有在与服务器校验校验票据信息成功之后,正确调用`nt_finishTransaction`来结束本次购买交易。只要交易不主动结束,重启 App 还会收到交易队列的更新信息 - keychain 持久化票据信息,票据校验失败后进行多次网络错误重试与持久化缓存票据重试 - 加入埋点统计,上报内购失败错误描述 ## 参考文档 App 内购买项目:[https://developer.apple.com/cn/in-app-purchase/](https://developer.apple.com/cn/in-app-purchase/) 自动续期订阅:[https://developer.apple.com/cn/app-store/subscriptions/#clear-description](https://developer.apple.com/cn/app-store/subscriptions/#clear-description) StoreKit 开发文档:[https://developer.apple.com/documentation/storekit](https://developer.apple.com/documentation/storekit) App 内购买项目中文文档:[https://developer.apple.com/cn/documentation/storekit/in-app_purchase/](https://developer.apple.com/cn/documentation/storekit/in-app_purchase/) 在线查找你的购买记录:[https://support.apple.com/zh-cn/HT204088](https://support.apple.com/zh-cn/HT204088) 针对从 Apple 购买的 App 或内容申请退款:[https://support.apple.com/zh-cn/HT204084](https://support.apple.com/zh-cn/HT204084) 提供 APP 内购买项目官方帮助文档:[https://developer.apple.com/cn/help/app-store-connect/configure-in-app-purchase-settings/overview-for-configuring-in-app-purchases/](https://developer.apple.com/cn/help/app-store-connect/configure-in-app-purchase-settings/overview-for-configuring-in-app-purchases/) 通过 App 内购买和订阅来购买额外的 App 功能:[https://support.apple.com/zh-cn/HT202023](https://support.apple.com/zh-cn/HT202023) 与家庭成员共享 Apple 提供的订阅:[https://support.apple.com/zh-cn/HT212253](https://support.apple.com/zh-cn/HT212253)
taozongqiang
2024年2月28日 14:00
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码