iOS_苹果内购详细步骤

iOS苹果内购详细步骤

iOS开发支付的两种方式

1 Apple Pay + 调取外部支付,例如支付宝、微信、银联等

什么是Apple Pay?

简单说是一种“支付工具”。对于国外流行信用卡,Apple Pay很符合美国的国情。但对于中国,微信支付、支付宝支付更加便利符合中国的人的行为习惯。
说到这你可能就理解了,Apple Pay,就是类比支付宝类似的线上线下支付工具。
Apple Pay和 支付宝、微信一样主要针对现实现金交易。当然也存在平台支持虚拟产品支付。例如淘宝平台出售虚拟物品,电话费、游戏卡等等。但这不属于苹果内购范畴。

当然对于我们的自己的App,我们可以接入多种支付方式。

2 苹果内购IAP(In-App Purchase)

什么是内购?

如果你购买的商品,是在本app中使用和消耗的,就一定要用内购,否则会被拒绝上线,例如:游戏币,在线书籍,app中使用的道具等虚拟产品。如果购买的就是普通的商品,例如淘宝买东西等,就不需要用内购。内购的话,苹果公司需要抽取30%左右佣金。

苹果内购价格表中的实际收入是动态变化的,会根据税收变化而发生改变,一般苹果会收取30%左右的金额。但是表格里边的价格和等级一般是不变的。

当然,打赏功能被纳入内购项目中。所以例如微信打赏功能、直播项目打赏主播都必须采用内购。

  • 可以简单理解成,带有内购功能的项目以后的成本会比安卓、PC端高出30%成本

  • 内购使用场景:爱奇艺APP购买会员,QQ斗地主里面的充值QB等。

  • 支付弹窗图标、价格、详情等都需要到https://developer.apple.com里面去设置。具体下面会讲到。

项目积分购买课程需使用内购方式,因此本文将按照政策规则、代码集成+丢包处理、税务填写银行账号信息等方面详细介绍IAP

1 IAP规则详解

  • 本文所述IAP(In-App Purchase),特指苹果App Store的应用内购买,是苹果为App内购买虚拟商品或服务提供的一套交易系统。
    首先来讨论一下IAP的基本规则以及其中的一些要点:

1.1 适用范围

  • 在App内需要付费使用的产品功能或虚拟商品/服务,如游戏道具、电子书、音乐、视频、订阅会员、App的高级功能等。
  • App内购买实体商品(如淘宝买衣服)不适用IAP,不在App内使用的虚拟商品(如充话费)或服务(如滴滴叫车)也不适用IAP。
    那么问题来了,假如在App内购买一个音乐专辑,既能在App里面听数字专辑,同时也能获得实体商品cd,适不适用IAP呢?
    答案是适用的。因为App内的数字专辑和实体商品cd在使用上是可以分离的,数字专辑符合IAP的适用范围,购买就要用IAP。否则各种游戏里面卖648的道具,都声称商品不仅包含游戏道具,购买后还能获得一个5毛钱的实体纪念品(举例),就直接绕过IAP,苹果岂不完蛋?
    苹果规定,适用范围内的虚拟商品或服务,必须使用IAP购买支付,不允许使用支付宝、微信支付等其它支付方式(包括Apple Pay),也不允许以任何方式(包括跳出App、提示文案等)引导用户通过应用外部渠道购买。
  • 原则上苹果也不允许通过外部兑换码等方式在应用内解锁虚拟商品或服务,但实际上兑换码的限制是有些模糊的,因为有些App可以在应用内获得兑换码(比如活动发放优惠券或签到奖励),很难严格界定是一个外部兑换码还是内容兑换码。因此,在IAP购买中使用优惠券抵扣一般情况下是允许的,但如果很明显地引导用户在App外购买兑换码,再在App内兑换成虚拟商品或服务,是会被苹果Reject的。
  • 另外在App Store Review Guidelines 3.1.4 里面还有这样一条特殊规则:

“App features that work in combination with an approved physical product (such as a toy) on an optional basis may unlock functionality without using IAP, provided that an IAP option is available as well.”

  • 意思就是说,用户在App内购买一个功能,而这个功能需要和一个实体商品结合起来使用,这种情况下,允许使用IAP之外的方式来解锁功能,但前提是仍然需要提供IAP购买的选项。
  • 比如用户在一个健康管理App内购买一个高级计步器功能,但这个计步器功能需要跟手环配合使用,要和手环打包购买。这个情况下,App可以在IAP购买的选项基础上,再提供其它的购买方式。
  • 但现实中好像并见过没有这样的案例。因为App完全可以把计步器变成免费功能,只是没有手环不起实际作用,然后把手环当成是一个在App外使用的实体商品,这样跟IAP就没有半毛钱关系了。谁没事找事还把这个东西做成IAP还要给苹果分成啊~
  • 另外还有一些跨平台同步的复杂案例,在本文的第三部分再进一步介绍。

1.2 IAP类型

  • 如前面说的,IAP是一套商品交易系统,而非简单的支付系统。每一个购买项目都需要在App的itunes connect后台创建一个商品,提交给苹果审核,审核通过后,购买项目才会生效。
  • 在创建IAP商品时,主要有4种类型可供选择:

1.2.1 Consumable products (消耗型商品)

  • 该类型适用于可多次购买的消耗型项目,如游戏道具、虚拟币等。

1.2.2 Non-consumable products(非消耗型商品)

  • 该类型适用于一次购买永久有效的项目,如电子书、游戏关卡等。
  • 该类型项目支持跨设备同步和本地restore,比如说,用户在某个App中购买了一本书,可在所有相同Apple ID设备的App中免费获取这本书,而不要需要借助App本身的帐号体系,即使在App中删除了这本书,也可免费重新获取。

1.2.3 Non-renewable subscriptions(非续期订阅)

  • 该类型适用于固定有效期的非自动续费项目,如云音乐的会员和一些视频App的会员。没有跨设备同步和本地restore机制,用户可以多次购买。

1.2.4 Auto-renewable subscriptions(自动续期订阅)

  • 该类型适用于自动续费的订阅项目,如Apple Music的按月订阅,用户购买后会每月自动续费,直到用户手动取消或者开发者下架IAP项目。
  • 类似Non-consumable products,该类型也支持跨设备同步和本地restore机制。
  • 之前这种类型只支持newsstand类别(报刊杂志)的App,从2016年6月开始支持所有类型的App,但除了newsstand类别之外,国内的App很少使用这种类型的内购。

1.2.4.1 Free subscriptions(免费订阅)

  • 该类型是Auto-renewable subscriptions的一个特例,适用于免费的订阅项目,仅支持newsstand类别的App,同样支持跨设备同步和本地restore机制。
    《In-App Purchase Programming Guide》详细说明了各种类型的适用范围和特性。

其中需要特别注意的是:

  • 1 针对Non-consumable products类型的IAP项目,苹果会要求App提供一个“恢复购买”的功能,以支持跨设备同步和本地restore。同时,如果App本身有用户帐号系统,那么用户只要付费一次,就可以通过restore机制将IAP项目无限复制到多个用户帐号下。
    因此,对于类似电子书之类的一次购买永久有效的项目,如果希望使用App本身的用户帐号系统,避开跨设备同步和本地restore机制,可以考虑选择Non-renewable subscriptions类型。同时,考虑到Non-renewable subscriptions一般是有固定有效期的,可以加一个无限长的有效期(比如9999天),以应对苹果审核。
  • 2 Consumable products和Non-renewable subscriptions都是可以重复购买的IAP项目,前者更偏向消耗品,后者更偏向订阅品。另外还有一个区别是,针对Non-renewable subscriptions的IAP项目,用户如果之前已经买过一次,过期后再次购买或者切换App帐号后购买,支付流程中会出现一个系统弹窗提示用户之前已经购买过该项目,是否要再次购买,如果用户不小心点了取消,支付流程就会终止。
    苹果设计这个弹窗的本意更多是根据Apple ID识别用户身份,避免用户重复购买相同项目。但对于有用户帐号体系的App,这个提示是有点多余的,虽然影响不大。因此,如果一个IAP项目既适用于Consumable products也适用于Non-renewable subscriptions,比较建议选择Consumable products。

1.3 定价

  • 在创建IAP项目的时候,需要设定价格,这个价格只能从苹果预设的价格等级中选择,比如等级1对应1美元、6元人民币,等级2对应2美元、12元人民币……最高等级87对应999.99美元、6498元人民币。另外可能是为了照顾某些货币区的开发者和用户,还有一些特殊的等级,比如备用等级A对应1美元、1元人民币,备用等级B对应1美元、3元人民币这样。除此之外,IAP项目不能定一个9.9元人民币这样不符合任何等级的价格。详细价格等级表可以看苹果的官方文档

  • 苹果的价格等级表通常是不会调整的,但也不排除在某些货币汇率发生巨大变化的情况下,对该货币的定价进行调整,调整前苹果会发邮件通知开发者。

  • 另外,价格等级表中不同货币的汇率关系与实际汇率存在差异,淘宝上有部分低价的iOS游戏内购代充就是利用了某些货币的汇率差来做生意。

  • 对于开发者来说,如果App在中国区以外发布,可能需要注意一下汇率的问题。某些地区的内购结算成人民币后收入会低于相应价格等级的人民币收入,所以在一些需要严格计算实际收入的情况下(比如财务统计营收,或内购收入需要进一步与平台CP方进行分成),可能需要根据内购的实际支付货币及金额和还有相应汇率来计算收入,或者也可以用一些办法来限制某些地区的内购(在本文2.2部分会讲)。

  • 补充说明一下,IAP项目的价格在商品发布以后是可以在itunes connect后台修改的,也可以设置限时优惠价。但是大部分联网App的内购项目价格是从自己的服务端获取的,如果要修改价格或设置限时优惠,需要两边一起处理,还是挺麻烦的。

1.4 分成

  • 很多人都知道,App Store上的付费App和App内购,苹果与开发者默认是3/7分成。
  • 但实际上,在某些地区苹果与开发者分成之前需要先扣除交易税,开发者的实际分成不一定是70%。从2015年10月开始,苹果对中国地区的App Store购买扣除了2%的交易税,对于中国区帐号购买的IAP,开发者的实际分成在68%~69%之间。而且中国以外不同地区的交易税标准也存在差异,如1.3中所述,如果需要严格计算实际收入,可能需要把这个部分也考虑进来。
  • 针对不同地区的内购,内购价格和对应的开发者实际收入在苹果的价格等级表(1.3中的链接)中有详细列举。
  • 另外,根据苹果在2016年6月的新规则,针对Auto-Renewable Subscription类型的IAP,如果用户购买的订阅时间超过1年,那么从第二年开始,开发者可以获得85%的分成。详情可查看:
    https://developer.apple.com/app-store/subscriptions/

1.5 结算

  • 针对IAP的交易收入,苹果一般以5周(每年1/4/7/10月)或4周(其余月份)作为一个结算周期,并在每个结算周期结束后第33天向开发者付账。

2 IAP设计开发要点

2.1 创建和提交IAP项目

  • 开发IAP之前需要先在itunes connect后台创建IAP商品,并按规范填写product id、商品名称、价格、截图等信息。
  • 如果App当前版本支持新增的IAP项目,可不用发版直接提交IAP审核。如果需要App新功能配合,则需要和App版本一起提交。
    《In-App Purchase Configuration Guide for iTunes Connect》详细介绍了IAP的创建和提交流程:

其中需要特别注意的是:

2.1.1 尽量不要删除已创建的IAP

  • 已创建的IAP除了product id之外的所有信息都可以修改,如果删除了一个IAP,将无法再创建一个相同product id的IAP,也意味着该product id永久失效。而product id一般有特定的命名规则,用来标示App内的购买项目,如果命名规则下有某个product id永久失效,可能会导致整个product id命名规则都要修改,掉进坑里~

2.1.2 注意区分reference name和display name

  • reference name是给开发者自己看的,display name会在IAP支付流程的确认购买系统弹窗中展示给用户,而且不能随意修改(修改需要重新提交IAP审核),所以命名的时候要弄清楚。

2.1.3 当App审核被拒时

  • 如果IAP随App版本一起提交审核,有问题时所有新提交的IAP项目和App版本会同时被拒,再次提交App审核时,一定要记得重新提交所有IAP项目(每个IAP还得要手动编辑一下才能重新提交真麻烦),否则苹果是无法继续审核的。

2.2 IAP支付流程

  • 这部分内容属于功能实现的逻辑,同样在《In-App Purchase Programming Guide》中有详细说明:

  • 具体来说,IAP的支付模式分为 客户端校验 服务端校验 2种模式,客户端校验模式因为容易伪造支付凭据,安全性比较低,一般只有非常简单的单机App才会使用,大部分App都会采用 服务端校验 模式。

另外不同类型的IAP支付流程也会有一些小差异(主要是restore机制),下面就以最常用的Consumable products和Non-renewable subscriptions类型为例,来说明一下IAP的支付流程:

请添加图片描述

步骤说明:

  1. 程序向服务器发送请求,获得一份产品列表。
  2. 服务器返回包含产品标识符的列表。
  3. 程序向App Store发送请求,得到产品的信息。
  4. App Store返回产品信息。
  5. 程序把返回的产品信息显示给用户(App的store界面)
  6. 用户选择某个产品
  7. 程序向App Store发送支付请求
  8. App Store处理支付请求并返回交易完成信息。
  9. 程序从信息中获得数据,并发送至服务器。
  10. 服务器纪录数据,并进行审(我们的)查。
  11. 服务器将数据发给App Store来验证该交易的有效性。
  12. App Store对收到的数据进行解析,返回该数据和说明其是否有效的标识。
  13. 服务器读取返回的数据,确定用户购买的内容。
  14. 服务器将购买的内容传递给程序。

以上简要步骤总结:

  • 用户进入购买虚拟物品页面,App从后台服务器获取产品列表然后显示给用户
  • 用户点击购买购买某一个虚拟物品,APP就发送该虚拟物品的productionIdentifier到Apple服务器
  • Apple服务器根据APP发送过来的productionIdentifier返回相应的物品的信息(描述,价格等)
  • 用户点击确认键购买该物品,购买请求发送到Apple服务器
  • Apple服务器完成购买后,返回用户一个完成购买的凭证
  • APP发送这个凭证到后台服务器验证
  • 后台服务器把这个凭证发送到Apple验证,Apple返回一个字段给后台服务器表明该凭证是否有效
  • 后台服务器把验证结果在发送到APP,APP根据验证结果做相应的处理

2.3 不能忽视的坑

2.3.1 延迟返回支付结果

  • 在上述流程的步骤6,由于网络问题等种种原因,即使用户已经付款成功,客户端也可能一时半会收不到苹果API的支付成功通知,也无法主动向苹果API请求查询支付状态,只能被动等待通知。
  • 因此有些情况下,客户端会延迟收到支付成功的通知(可能是过了几分钟,也有可能是下次打开App的时候),针对这种情况,需要做好两件事:
  • 1 客户端本地保存所有支付结果未确认的交易信息,并设置一个监听进程,在收到支付成功的信息后,继续处理这笔交易的后续流程。极端情况下,用户在交易结果未确认的情况下删除App,保存在App本地数据库中的交易信息也会丢失,因此,更好的方案是 把交易信息存到iOS系统的keychain里面
  • 2 当本地存在支付结果未确认的交易信息时,在交互上提示用户可能需要等待支付结果,避免用户重复付款

2.3.2 服务端校验延迟

在上述流程6~8的支付凭据校验过程中,因为网络问题等各种原因,客户端可能无法及时收到服务端的校验成功通知,类似的,这种情况需要:

  • 1 客户端本地保存支付凭据,并持续向服务端轮询校验结果,直到返回明确的校验成功或校验无效的结果。支付凭据最好也保存在 keychain 里面
  • 2 在客户端向服务端轮询结果时,为了避免用户在支付结果页面等待过久,交互层面上可以先结束支付流程(经过一定时间超时),同时提示用户需要等待支付结果,避免用户重复付款

2.3.3 非官方渠道包支付失败问题

  • 在上述流程步骤1中,如果用户安装的App不是App Store官方渠道包(从PP助手、同步推等第三方应用商店下载),苹果API会直接返回product id不存在并结束支付流程,在交互层面上表现为用户点击购买后直接提示支付失败。

  • 类似的,越狱设备在安装某些内购破解插件后,也会导致无法进行内购(返回product id不存在)。不过现在iOS设备的越狱比例已经非常低了,基本可以忽略。

  • 因此,针对这个问题的解决办法是:当返回product id不存在时,提示用户安装的可能是非官方渠道包,引导用户到App Store下载官方渠道包。

2.3.4 货币校验

  • 如本文1.3部分所述,如果App在中国区以外发布,但由于某些原因又希望限制某些地区帐号的内购,可以在上述流程的步骤4校验用户支付的货币单位,并禁止某些货币的购买。同时在交互上也要给用户相应的提示,类似不支持特殊地区帐号购买的弹窗说明。

2.3.5 未绑定App Store支付方式的用户支付流程

这是一个巨大的坑!

  • 如果用户在内购前未绑定App Store的支付方式,在上述流程的步骤5,点击系统弹窗(第1次)的确认购买后,会自动跳转到App Store的绑定支付方式界面。接着如果顺利完成支付方式的绑定,会再自动跳转跳回App,再次出现系统弹窗(第2次)让用户确认购买。
  • 但是,当用户点击系统弹窗(第1次)的确认购买后,苹果API会立即向客户端返回支付失败…支付失败…失败…一般情况下,客户端收到支付失败的返回,理所应当认为支付已经取消了,而丢弃本地的交易信息。但万万没想到后面用户绑定支付方式完成后会继续确认购买,这简直就是苹果IAP系统设计的一个大bug!
  • 如果知道了这个坑,解决方案也很简单,就是在苹果API返回支付失败时,用类似2.3.1的方式处理,保留待确认的交易信息并持续监听支付结果返回。

3 更多IAP实践经验总结

3.1 匿名购买

  • 很多App在第一次提交IAP时都会因为不支持匿名购买被拒,原因是App Store Review Guidelines要求App在非必要的情况下,不允许强制用户注册/登录后才能使用某些功能。
  • 一般情况下,对于没有IAP的App,强制用户注册/登录才能使用是不会被拒的(除非遇到很苛刻的审核人员),但是对于IAP,一般都会要求支持匿名购买。当然你也可以尝试编一堆原因说明为什么强制用户注册/登录是必要的(比如提供的商品或服务需要获取用户手机号或邮箱之类的理由),但也不能保证说服审核人员。
  • 支持匿名购买,通常需要在用户未登录App帐号的情况下,临时保存用户的购买记录,并在用户登录后合并到App帐号数据。

3.2 跨平台同步

虽然在原则上,苹果不允许通过外部渠道解锁App内需要付费才能使用的功能或虚拟商品,比如在web端购买一个课程,在App内观看,但实际在某些条件下是可以做跨平台同步用户购买的内容:

  • 1 在iOS App内也提供相应商品IAP购买的前提下,通过App帐号同步用户在不同平台上购买的内容。这条规则适用到大部分游戏上可能会比较苛刻,但是一般的内容类App都是行的通的。
  • 2 针对电子书、音乐、视频等项目,App只提供单纯的内容阅读/观看功能,而不提供任何发现和订阅内容的功能(比如Kindle iOS版),可支持用户在外部渠道购买内容后在App中使用(具体可参考App Store Review Guidelines 3.1.3)。但实际上是否允许可能也要取决于审核人员的判断,比如云课堂曾经提交过一个版本,不能购买付费课程,但是允许用户在web端购买付费课程后在iOS端观看,就被拒了,理由是付费课程不属于上述条件的内容范畴。

3.3 虚拟币

  • 由于IAP的价格等级机制,无法支持灵活的商品定价(如9.9元人民币)和营销功能(如优惠券抵扣等),很多App会引入虚拟币,先通过IAP内购充值特定价格的虚拟币(如6、12、18、648等),再使用虚拟币购买具体的商品。
  • 类似IAP跨平台同步问题,iOS平台充值的虚拟币不允许和其他平台(Android、web)流通,外部平台充值的虚拟币不能在iOS平台使用,同时iOS平台充值的虚拟币也不能在外部平台使用。
  • 但实际中,我们会发现有些App,比如喜马拉雅和得到,支持用户在微信公众号中充值虚拟币(虽然微信充值也区分了iOS平台和其他平台),再在App内使用,并且在App内的充值界面,隐晦的提示用户如果充值遇到问题,可以关注微信公众号获取客服帮助(实际上就是引导用户用微信充值)。这种做法严格意义上是不合规的,但如果苹果审核不严也能通过,算是打擦边球,估计这招效果还不错~

3.4 退款问题

  • 根据苹果政策,用户在购买IAP后90天内,能以各种原因申请退款(扣款后购买失败、买错了、不喜欢等等),但实际能不能退成功苹果说了算。
  • 如果一个用户申请退款成功,苹果会在与开发者结算时记一笔退款订单(当然原来购买成功的订单也在),钱就不给了,而且苹果也不会告诉你是哪个用户退款的,而且用户购买的东西还在,简直就是霸王条件~
  • 订单数据可以在itunes connect后台的“付款和财务报告”中查看,但没有详细时间信息和用户信息,很难定位到相应的平台订单。
  • 对于游戏或者一些自营平台来说,可能就是少点收入。而对于需要与平台CP方一一分成的产品来说,可能就要亏钱。为了解决这个问题,可设计一套退款订单分配规则,根据IAP订单的时间周期、金额,结合平台订单的一些信息,将IAP退款订单分配到平台订单,由平台和CP方共同承担退款。
  • 从历史数据来看,一般不遇到大量恶意退款的情况下,IAP的退款率可能在1~3%之间(取决于App内容质量、用户心情,还有苹果的心情)。对于IAP营收额很大的App,可能需要考虑一下退款的问题。

3.5 支付成功率

  • 云课堂曾经对比过iPhone端使用支付宝/微信支付(注意:绕过IAP使用第三方支付属于违规行为,可能导致应用下架或开发者被封号,切勿模仿!)和IAP的支付数据,发现第三方支付的用户支付成功率大概是IAP的2~3倍,反映在营收上也差了2倍多。
    不难理解,国内App Store的用户支付习惯肯定不如第三方支付成熟。而经过进一步的用户问卷调研,发现影响IAP支付成功率的因素有很多方面:
  • 1 用户安装的App来自第三方应用商店或使用越狱设备,无法使用IAP支付(约5%用户)
  • 2 用户未绑定App Store支付方式(约75%用户,数据很惊人)
  • 3 用户在IAP支付过程绑定支付方式遇到问题,放弃了(约50%用户)
  • 以上数据仅代表云课堂iPhone端的用户群,某种意义上也说明云课堂的用户比较小白(产品本身的目标用户很大一部分就是小白),没接触过IAP这种支付方式。不同产品的目标用户群的App Store内购使用习惯肯定也存在差异。
  • 不过,随着App Store内购的普及(一方面是游戏内购带动,另一方面也得益于苹果的推广,比如经常搞一些App一元购活动,还有从2016年11月开始支持绑定支付宝),用户的内购习惯也在持续养成,从数据上也能看出IAP支付成功率的上升趋势,对于接入IAP的产品是友好的。

3.6 作弊行为

考虑到IAP的分成、退款以及支付成功率等一系列问题,虽然我们有很多种办法可以偷偷绕过IAP使用第三方支付,比如:

  • 1 通过后台开关控制在某些时间开启第三方支付
  • 2 根据用户信息和行为识别出明显不是苹果审核人员的用户(特别是其中一些高客单价用户),开放第三方支付
    等等……
    但是根据公司企业发展部、市场部、法务部的要求,切勿在IAP的问题上搞事情!好好遵守苹果规则,做好产品,争取App Store推荐。

4 代码集成

内购所需要写的代码是非常少的,只有几百行。

4.1 引入头文件

import StoreKit

4.2 遵循两个代理

SKProductsRequestDelegate, SKPaymentTransactionObserver

4.3 开启内购检测

func add(_ observer: SKPaymentTransactionObserver)

let paymentQueue: PaymentQueue = SKPaymentQueue.default()
paymentQueue.add(self)

4.4 设置内购点击事件

private func purchase(
    product: SKProduct, 
    quantity: Int,
    atomically: Bool,      
    applicationUsername: String = "", 
    completion: @escaping (PurchaseResult) -> Void
) {
    
    
    guard SwiftyStoreKit.canMakePayments else {
    
    
        let error = NSError(
          domain: SKErrorDomain, 
          code: SKError.paymentNotAllowed.rawValue,
          userInfo: nil
        )
        completion(.error(error: SKError(_nsError: error)))
        return
    }

    paymentQueueController.startPayment(
      Payment(
        product: product, 
        quantity: quantity, 
        atomically: atomically, 
        applicationUsername: applicationUsername
      ) {
    
     result in
        completion(self.processPurchaseResult(result))
    })
}

4.5 请求商品,获取商品信息,以及代理

private let request: SKProductsRequest
deinit {
    
    
    request.delegate = nil
}
private init(
  productIds: Set<String>,
  callback: @escaping RequestCallback
) {
    
    
    self.callback = callback
    request = SKProductsRequest(productIdentifiers: productIds)
    super.init()
    request.delegate = self
}

class func startQuery(
  _ productIds: Set<String>, 
  callback: @escaping RequestCallback
) -> InAppProductQueryRequest {
    
    
    let request = InAppProductQueryRequest(productIds: productIds, callback: callback)
    request.start()
    return request
}

func start() {
    
    
    request.start()
}
func cancel() {
    
    
    request.cancel()
}

// MARK: SKProductsRequestDelegate
func productsRequest(
  _ request: SKProductsRequest,
  didReceive response: SKProductsResponse
) {
    
    
    let retrievedProducts = Set<SKProduct>(response.products)
    let invalidProductIDs = Set<String>(response.invalidProductIdentifiers)
    performCallback(RetrieveResults(retrievedProducts: retrievedProducts,
        invalidProductIDs: invalidProductIDs, error: nil))
}

func requestDidFinish(_ request: SKRequest) {
    
    

}

func request(
  _ request: SKRequest, 
  didFailWithError error: Error
) {
    
    
    performCallback(
      RetrieveResults(
        retrievedProducts: Set<SKProduct>(), 
        invalidProductIDs: Set<String>(), 
        error: error
      )
    )
}

private func performCallback(_ results: RetrieveResults) {
    
    
    DispatchQueue.main.async {
    
    
        self.callback(results)
    }
}

4.6 沙盒测试环境验证以及正式环境

public enum VerifyReceiptURLType: String {
    
    
    // 正式环境验证
		case production = "https://buy.itunes.apple.com/verifyReceipt"
    // 沙盒测试环境验证
		case sandbox = "https://sandbox.itunes.apple.com/verifyReceipt"
}

public init(service: VerifyReceiptURLType = .production) {
    
    
    self.service = service
}

private let service: VerifyReceiptURLType

public func validate(
  receipt: String,
  password autoRenewPassword: String? = nil,
  completion: @escaping (VerifyReceiptResult) -> Void) {
    
    

  let storeURL = URL(string: service.rawValue)! // safe (until no more)
  let storeRequest = NSMutableURLRequest(url: storeURL)
  storeRequest.httpMethod = "POST"

  let requestContents: NSMutableDictionary = [ "receipt-data": receipt ]
  // password if defined
      if let password = autoRenewPassword, !password.isEmpty {
    
    
    requestContents.setValue(password, forKey: "password")
  }

  // Encore request body
  do {
    
    
    storeRequest.httpBody = try JSONSerialization.data(withJSONObject: requestContents, options: JSONSerialization.WritingOptions.prettyPrinted)
  } catch let e {
    
    
    completion(.error(error: .requestBodyEncodeError(error: e)))
    return
  }

  // Remote task
  let task = URLSession.shared.dataTask(with: storeRequest as URLRequest) {
    
     data, _, error -> Void in

    // there is an error
    if let networkError = error {
    
    
      completion(.error(error: .networkError(error: networkError)))
      return
    }

    // there is no data
    guard let safeData = data else {
    
    
      completion(.error(error: .noRemoteData))
      return
    }

    // cannot decode data
    guard let receiptInfo = try? JSONSerialization.jsonObject(with: data!, options: .mutableLeaves) as? ReceiptInfo ?? [:] else {
    
    
      let jsonStr = String(data: safeData, encoding: String.Encoding.utf8)
      completion(.error(error: .jsonDecodeError(string: jsonStr)))
      return
    }

    // get status from info
    if let status = receiptInfo["status"] as? Int {
    
    
      let receiptStatus = ReceiptStatus(rawValue: status) ?? ReceiptStatus.unknown
      if case .testReceipt = receiptStatus {
    
    
        let sandboxValidator = AppleReceiptValidator(service: .sandbox)
        sandboxValidator.validate(receipt: receipt, password: autoRenewPassword, completion: completion)
      } else {
    
    
        if receiptStatus.isValid {
    
    
          completion(.success(receipt: receiptInfo))
        } else {
    
    
          completion(.error(error: .receiptInvalid(receipt: receiptInfo, status: receiptStatus)))
        }
      }
    } else {
    
    
      completion(.error(error: .receiptInvalid(receipt: receiptInfo, status: ReceiptStatus.none)))
    }
  }
  task.resume()
}

5 IAP QA测试帮助

IAP沙盒测试账号使用

6 App Store协议、税务和银行业务

6.1 登录iTunes Connect

  1. 登录iTunes Connect

  2. 进入协议、税务和银行业务页面

    请添加图片描述

6.2 选择申请合同类型

进入协议、税务和银行业务页面后,会有3种合同类型,如果你之前没有主动申请过去合同,那么一般你现在激活的合同只有iOS Free Application一种。

页面内容分为两块:

  • Request Contracts(申请合同)
  • Contracts In Effect(已生效合同)。

合同类型分为3种:

  • iOS Free Application(免费应用合同)
  • iOS Paid Application(付费应用合同)
  • iAd App NetNetwork(广告合同)

6.3 申请iOS Paid Application合同

  • 当我们点击申请iOS Paid Application合同后,该合同的状态会变成如下的样子,我们可以看到其中Status为Pending Tax, Bank, Contact。意思是联系方式、银行和税务信息没有填写。
    请添加图片描述

6.4 添加银行账户

在这里插入图片描述
在这里插入图片描述

  • 填写银行CNAPS Code(ABA汇款路线号码)

在这里插入图片描述

6.5 报税(跟咱没啥关系,但还是要填)

在这里插入图片描述

默认美国打勾,其他有的话打勾,一般都没

在这里插入图片描述

选择美国U.S Tax Forms,选择后会问你两个问题

  • 第一个问题如下:询问你是否是美国居民,有没有美国伙伴关系或者美国公司,如果没有直接选择No。

在这里插入图片描述

  • 第二个问题如下:询问你有没有在美国的商业性活动,没有也直接选No。

在这里插入图片描述

6.5.1 填写税务信息

包括以下几点:

  • Individual or Organization Name:个人或者组织名称
  • Country of incorporation: 所在国家
  • Type of Beneficial Owner:受益方式,独立开发者选个人
  • Permanent Residence:居住地址
  • Mailing address:邮寄地址
  • Name of Person Making this Declaration:声明人
  • Title:头衔

填写完这些信息后就可以提交了
请添加图片描述

6.5.2 填写联系信息,如果是个人开发者就全写自己

请添加图片描述

6.6 等待审核

当你填写完所有资料后,合同状态就会变成Processing,大约半天就通过了。

请添加图片描述

猜你喜欢

转载自blog.csdn.net/FlyingKuiKui/article/details/129182829