内购有4种:消耗型项目,非消耗型,自动续期订阅,非续期订阅。 其中”非消耗型“和”自动续期订阅“需要提供恢复购买的功能,例如创建一个恢复按钮,不然审核很可能会被拒绝。
</div><div class="CodeMirror-scrollbar-filler" cm-not-content="true"/><div class="CodeMirror-gutter-filler" cm-not-content="true"/><div class="CodeMirror-scroll" tabindex="-1"><div class="CodeMirror-sizer" style="margin-left: 0px; margin-bottom: 0px; border-right-width: 0px; padding-right: 0px; padding-bottom: 0px;"><div style="position: relative; top: 0px;"><div class="CodeMirror-lines" role="presentation"><div role="presentation" style="position: relative; outline: none;"><div class="CodeMirror-measure"><pre><span>xxxxxxxxxx</span></pre></div><div class="CodeMirror-measure"/><div style="position: relative; z-index: 1;"/><div class="CodeMirror-code" role="presentation"><div class="CodeMirror-activeline" style="position: relative;"><div class="CodeMirror-activeline-background CodeMirror-linebackground"/><div class="CodeMirror-gutter-background CodeMirror-activeline-gutter" style="left: 0px; width: 0px;"/><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">//调起苹果内购恢复接口</span></pre></div><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];</span></pre></div></div></div></div></div><div style="position: absolute; ; width: 1px; border-bottom-width: 0px; border-bottom-style: solid; border-bottom-color: transparent; top: 52px;"/><div class="CodeMirror-gutters" style="display: none; height: 52px;"/></div></div></pre><p>“消耗型项目”和“非续期订阅”苹果不会提供恢复的接口,不要调用上述方法去恢复,否则有可能被拒。 “非续期订阅”也是“跨设备同步”的,所以原则上来说也需要提供恢复购买的功能,但需要依靠app自建的账户体系恢复,不能用上述苹果提供的接口。</p><p>恢复购买将为用户完成的每个事务创建一个新事务,基本上是为事务队列观察者重新播放历史记录。</p><h3><a name="header-n16598" class="md-header-anchor "/>事务(Transaction)</h3><p>内购事务的观察者应尽早设置好,例如在程序启动后马上设置:</p><pre spellcheck="false" class="md-fences md-end-block contain-cm modeLoaded" lang=""><div class="CodeMirror cm-s-inner CodeMirror-wrap"><div style="overflow: hidden; position: relative; width: 3px; ; top: 4px; left: 4px;"><textarea autocorrect="off" autocapitalize="off" spellcheck="false" tabindex="0" style="position: absolute; bottom: -1em; padding: 0px; width: 1000px; height: 1em; outline: none;"/></div><div class="CodeMirror-scrollbar-filler" cm-not-content="true"/><div class="CodeMirror-gutter-filler" cm-not-content="true"/><div class="CodeMirror-scroll" tabindex="-1"><div class="CodeMirror-sizer" style="margin-left: 0px; margin-bottom: 0px; border-right-width: 0px; padding-right: 0px; padding-bottom: 0px;"><div style="position: relative; top: 0px;"><div class="CodeMirror-lines" role="presentation"><div role="presentation" style="position: relative; outline: none;"><div class="CodeMirror-measure"><pre><span>xxxxxxxxxx</span></pre></div><div class="CodeMirror-measure"/><div style="position: relative; z-index: 1;"/><div class="CodeMirror-code" role="presentation" style=""><div class="CodeMirror-activeline" style="position: relative;"><div class="CodeMirror-activeline-background CodeMirror-linebackground"/><div class="CodeMirror-gutter-background CodeMirror-activeline-gutter" style="left: 0px; width: 0px;"/><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">- (BOOL)application:(UIApplication *)application</span></pre></div><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;"> didFinishLaunchingWithOptions:(NSDictionary *)launchOptions</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">{</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;"> /* ... */</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;"> [[SKPaymentQueue defaultQueue] addTransactionObserver:observer];</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">}</span></pre></div></div></div></div></div><div style="position: absolute; ; width: 1px; border-bottom-width: 0px; border-bottom-style: solid; border-bottom-color: transparent; top: 140px;"/><div class="CodeMirror-gutters" style="display: none; height: 140px;"/></div></div></pre><p>完成事务要调用:</p><pre spellcheck="false" class="md-fences md-end-block contain-cm modeLoaded" lang=""><div class="CodeMirror cm-s-inner CodeMirror-wrap"><div style="overflow: hidden; position: relative; width: 3px; ; top: 4px; left: 4px;"><textarea autocorrect="off" autocapitalize="off" spellcheck="false" tabindex="0" style="position: absolute; bottom: -1em; padding: 0px; width: 1000px; height: 1em; outline: none;"/></div><div class="CodeMirror-scrollbar-filler" cm-not-content="true"/><div class="CodeMirror-gutter-filler" cm-not-content="true"/><div class="CodeMirror-scroll" tabindex="-1"><div class="CodeMirror-sizer" style="margin-left: 0px; margin-bottom: 0px; border-right-width: 0px; padding-right: 0px; padding-bottom: 0px;"><div style="position: relative; top: 0px;"><div class="CodeMirror-lines" role="presentation"><div role="presentation" style="position: relative; outline: none;"><div class="CodeMirror-measure"><pre><span>xxxxxxxxxx</span></pre></div><div class="CodeMirror-measure"/><div style="position: relative; z-index: 1;"/><div class="CodeMirror-code" role="presentation"><div class="CodeMirror-activeline" style="position: relative;"><div class="CodeMirror-activeline-background CodeMirror-linebackground"/><div class="CodeMirror-gutter-background CodeMirror-activeline-gutter" style="left: 0px; width: 0px;"/><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">[[SKPaymentQueue defaultQueue] finishTransaction:transaction];</span></pre></div></div></div></div></div></div><div style="position: absolute; ; width: 1px; border-bottom-width: 0px; border-bottom-style: solid; border-bottom-color: transparent; top: 30px;"/><div class="CodeMirror-gutters" style="display: none; height: 30px;"/></div></div></pre><p>完成事务会告诉StoreKit你已经完成了购买所需的一切。未完成的事务将一直留在队列中,直到它们完成为止,每次启动应用程序时都会调用事务队列观察者,以便应用程序能够继续去完成未被完成的事务。你的应用需要完成每一笔交易,不管交易成功还是失败。 需要注意:在你完成一个事务之后,不应该再对这次交易做任何操作,例如交付产品或者验证交易是否有效等。如果还有工作要做,那证明你还没有准备好完成这个事务。应在所有必要的工作完成后,再调用完成事务的方法。</p><h3><a name="header-n16608" class="md-header-anchor "/>购买后的记录依据</h3><p>对于iOS 7及以后的非消耗品和自动续期订阅,使用app收据作为你的持久记录。 对于iOS 7之前版本的非消耗品和自动续期订阅,请使用用户默认系统或iCloud保存持久记录。 对于非续期的订阅,请使用iCloud或你自己的服务器来保存持久记录。 对于消耗品,应用程序更新其内部状态以反映购买情况,但不需要保存持久记录,因为消耗品不会跨设备恢复或同步。</p><h3><a name="header-n16614" class="md-header-anchor "/>验证收据</h3><p>应用程序收据包含用户购买的记录,由苹果公司以密码签署。 对于消耗型项目,信息在付款时添加到收据中,并保留在收据中,直到你完成交易。在你完成交易之后,该信息将在下一次更新收据时被删除——例如,下一次用户进行购买时。 所有其他类型的购买信息在付款时被添加到收据中,并无限期地保留在收据中。</p><p>收据的验证应该在服务端去做,这样更加安全。 自动续期订阅验证收据时必须要带上密钥。</p><p>获取receiptData:</p><pre spellcheck="false" class="md-fences md-end-block contain-cm modeLoaded" lang=""><div class="CodeMirror cm-s-inner CodeMirror-wrap"><div style="overflow: hidden; position: relative; width: 3px; ; top: 4px; left: 4px;"><textarea autocorrect="off" autocapitalize="off" spellcheck="false" tabindex="0" style="position: absolute; bottom: -1em; padding: 0px; width: 1000px; height: 1em; outline: none;"/></div><div class="CodeMirror-scrollbar-filler" cm-not-content="true"/><div class="CodeMirror-gutter-filler" cm-not-content="true"/><div class="CodeMirror-scroll" tabindex="-1"><div class="CodeMirror-sizer" style="margin-left: 0px; margin-bottom: 0px; border-right-width: 0px; padding-right: 0px; padding-bottom: 0px;"><div style="position: relative; top: 0px;"><div class="CodeMirror-lines" role="presentation"><div role="presentation" style="position: relative; outline: none;"><div class="CodeMirror-measure"><pre><span>xxxxxxxxxx</span></pre></div><div class="CodeMirror-measure"/><div style="position: relative; z-index: 1;"/><div class="CodeMirror-code" role="presentation" style=""><div class="CodeMirror-activeline" style="position: relative;"><div class="CodeMirror-activeline-background CodeMirror-linebackground"/><div class="CodeMirror-gutter-background CodeMirror-activeline-gutter" style="left: 0px; width: 0px;"/><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">// Load the receipt from the app bundle.</span></pre></div><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">if (!receiptData) { /* No local receipt -- handle the error. */ }</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">/* ... Send the receipt data to your server ... */</span></pre></div></div></div></div></div><div style="position: absolute; ; width: 1px; border-bottom-width: 0px; border-bottom-style: solid; border-bottom-color: transparent; top: 118px;"/><div class="CodeMirror-gutters" style="display: none; height: 118px;"/></div></div></pre><p>测试环境url:<a href="https://sandbox.itunes.apple.com/verifyReceipt" target="_blank" class="url">https://sandbox.itunes.apple.com/verifyReceipt</a> 正式环境url:<a href="https://buy.itunes.apple.com/verifyReceipt" target="_blank" class="url">https://buy.itunes.apple.com/verifyReceipt</a> 获取收据数据:</p><pre spellcheck="false" class="md-fences md-end-block contain-cm modeLoaded" lang="" style="page-break-inside: unset;"><div class="CodeMirror cm-s-inner CodeMirror-wrap"><div style="overflow: hidden; position: relative; width: 3px; ; top: 4px; left: 4px;"><textarea autocorrect="off" autocapitalize="off" spellcheck="false" tabindex="0" style="position: absolute; bottom: -1em; padding: 0px; width: 1000px; height: 1em; outline: none;"/></div><div class="CodeMirror-scrollbar-filler" cm-not-content="true"/><div class="CodeMirror-gutter-filler" cm-not-content="true"/><div class="CodeMirror-scroll" tabindex="-1"><div class="CodeMirror-sizer" style="margin-left: 0px; margin-bottom: 0px; border-right-width: 0px; padding-right: 0px; padding-bottom: 0px;"><div style="position: relative; top: 0px;"><div class="CodeMirror-lines" role="presentation"><div role="presentation" style="position: relative; outline: none;"><div class="CodeMirror-measure"><pre><span>xxxxxxxxxx</span></pre></div><div class="CodeMirror-measure"/><div style="position: relative; z-index: 1;"/><div class="CodeMirror-code" role="presentation" style=""><div class="CodeMirror-activeline" style="position: relative;"><div class="CodeMirror-activeline-background CodeMirror-linebackground"/><div class="CodeMirror-gutter-background CodeMirror-activeline-gutter" style="left: 0px; width: 0px;"/><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">//先对receiptData用base64编码</span></pre></div><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">NSString *receiptBase64 = [receiptData base64EncodedStringWithOptions:0];</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">//请求参数包含两个字段:“receipt-data”和”password“,其中”password“是自动续期订阅的密钥,其他类型的内购没有这个密钥就不会加这个参数。</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">NSDictionary *params = @{@"receipt-data":receiptBase64, @"password":secretKey};</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">//将params转成jsonData</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">NSData *jsonData = [NSJSONSerialization dataWithJSONObject:params</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">options:NSJSONWritingPrettyPrinted</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">error:nil];</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">//下面就是网络请求,例如</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">NSMutableURLRequest *req = [[NSMutableURLRequest alloc] initWithURL:url];</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">[req setHTTPMethod:@"POST"];</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">[req setHTTPBody:jsonData];</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:req delegate:self];</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">...</span></pre></div></div></div></div></div><div style="position: absolute; ; width: 1px; border-bottom-width: 0px; border-bottom-style: solid; border-bottom-color: transparent; top: 360px;"/><div class="CodeMirror-gutters" style="display: none; height: 360px;"/></div></div></pre><p>解析收据: 请求成功后返回的数据类似这个格式:</p><pre spellcheck="false" class="md-fences md-end-block contain-cm modeLoaded" lang=""><div class="CodeMirror cm-s-inner CodeMirror-wrap"><div style="overflow: hidden; position: relative; width: 3px; ; top: 4px; left: 4px;"><textarea autocorrect="off" autocapitalize="off" spellcheck="false" tabindex="0" style="position: absolute; bottom: -1em; padding: 0px; width: 1000px; height: 1em; outline: none;"/></div><div class="CodeMirror-scrollbar-filler" cm-not-content="true"/><div class="CodeMirror-gutter-filler" cm-not-content="true"/><div class="CodeMirror-scroll" tabindex="-1"><div class="CodeMirror-sizer" style="margin-left: 0px; margin-bottom: 0px; border-right-width: 0px; padding-right: 0px; padding-bottom: 0px;"><div style="position: relative; top: 0px;"><div class="CodeMirror-lines" role="presentation"><div role="presentation" style="position: relative; outline: none;"><div class="CodeMirror-measure"><pre><span>xxxxxxxxxx</span></pre></div><div class="CodeMirror-measure"/><div style="position: relative; z-index: 1;"/><div class="CodeMirror-code" role="presentation" style=""><div class="CodeMirror-activeline" style="position: relative;"><div class="CodeMirror-activeline-background CodeMirror-linebackground"/><div class="CodeMirror-gutter-background CodeMirror-activeline-gutter" style="left: 0px; width: 0px;"/><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">{"status": , //如果收据有效,则为0,其他值表示错误</span></pre></div><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;"> "receipt": , </span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;"> "latest_receipt": ,</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;"> "latest_receipt_info": , //只返回包含自动更新订阅的收据。这个键的值是一个包含所有应用程序内购买交易的数组。这排除了已经被你的应用标记为完成的消耗品的交易。</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;"> "latest_expired_receipt_info": ,</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;"> "pending_renewal_info": ,</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;"> "is-retryable": ,}</span></pre></div></div></div></div></div><div style="position: absolute; ; width: 1px; border-bottom-width: 0px; border-bottom-style: solid; border-bottom-color: transparent; top: 184px;"/><div class="CodeMirror-gutters" style="display: none; height: 184px;"/></div></div></pre><p>对于收据中每个字段的说明,参考苹果官方文档: <a href="https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104-SW1" target="_blank" class="url">https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104-SW1</a></p><p>这里说明一下自动续期订阅的验证问题: latest_receipt_info中包含所有订阅的交易信息,这是一个数组,里面每个元素代表一次订阅交易,元素中有expires_date_ms的字段表示订阅的过期时间,purchase_date_ms表示订阅的购买时间,product_id表示订阅的产品id,is_trial_period表示是否是免费试用。 需要注意,latest_receipt_info中的元素没有特定的排序,我们应该先将latest_receipt_info中的元素按expires_date_ms升序排序,然后获取最后一个元素,这个元素就是最新的交易信息。 另外,收据的json解析有可能会出错,要注意错误处理。</p><h3><a name="header-n16642" class="md-header-anchor "/>订阅后联系苹果退款</h3><p>订阅在购买时全额支付。用户只有联系苹果客服才能获得退款。例如,如果用户不小心购买了错误的产品,可以取消订阅,并发出全额或部分退款。 要检查购买是否已经被“Apple客户支持”取消,请在收据中查找cancel Date字段。如果字段包含日期,无论订阅的过期日期如何,购买都已取消。</p><h3><a name="header-n16646" class="md-header-anchor "/>自动续期订阅的免费试用(促销价)</h3><p>假如订阅设定了促销价,例如免费试用,SKProduct对象的introductoryPrice属性会有值,这个属性包含了促销价的信息,开发者应该根据这个属性去显示促销的相关UI。</p><h3><a name="header-n16649" class="md-header-anchor "/>“非续期订阅”不同于”自动续期订阅“的地方</h3><p>非续期订阅与自动续期订阅在几个关键方面有所不同。这些差异使你的应用程序具有灵活性,可以根据你的需要实现正确的行为,对于“非续期订阅”: