任何(早期)iOS自动续订订阅体验 [英] Any (early) experiences with auto-renewable subscriptions for iOS

查看:529
本文介绍了任何(早期)iOS自动续订订阅体验的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

Apple终于推出了所谓的昨天的自动续订订阅。由于我只有很少(仅限沙箱)的应用内购买经验,我不确定我是否在这里完成了所有工作。似乎一个需要服务器端验证收据。似乎找出订阅是否仍然有效的唯一方法是将原始交易数据存储在服务器端。关于这个主题的苹果编程指南对我来说都很神秘。



我的期望是,我只能通过商店套件询问iTunes,只能使用iOS客户端api他/她已经购买了这个(订阅)产品并且在一起收到了是/否答案



有没有人有自动再生订阅的经验或(因为它们看起来有点相似)非消费品?有关于此的任何好的教程吗?



谢谢。

解决方案

我在沙箱中运行它,几乎上线...



应该使用服务器验证收据。



在服务器上您可以使用收据数据记录设备udid,因为收据总是新生成的,并且它可以在多个设备上运行,因为收据总是新生成的。



开设备不需要存储任何敏感数据,并且不应该:)



每当应用程序出现时,应该检查商店的最后一个收据。应用程序调用服务器,服务器验证商店。只要商店返回有效的收据应用程序就可以使用该功能。



我开发了一个Rails3.x应用程序来处理服务器端,验证的实际代码看起来像这个:

  APPLE_SHARED_PASS =enter_yours
APPLE_RECEIPT_VERIFY_URL =https://sandbox.itunes.apple.com/ verifyReceipt#test
#APPLE_RECEIPT_VERIFY_URL =https://buy.itunes.apple.com/verifyReceipt#real
def self.verify_receipt(b64_receipt)
json_resp = nil
url = URI.parse(APPLE_RECEIPT_VERIFY_URL)
http = Net :: HTTP.new(url.host,url.port)
http.use_ssl = true
http.verify_mode = OpenSSL :: SSL :: VERIFY_NONE
json_request = {'receipt-data'=> b64_receipt,'password'=> APPLE_SHARED_PASS} .to_json
resp,resp_body = http.post(url.path,json_request.to_s,{'Content-Type'=>'application / x-www-form-urlencoded'})
if resp.code =='200'
json_resp = JSON.parse(resp_body)
logger.infoverify_receipt response:#{json_resp}
end
json_resp
end
#App Store错误响应
#21000 App Store无法读取您提供的JSON对象。
#21002 receipt-data属性中的数据格式不正确。
#21003收据无法通过身份验证。
#21004您提供的共享密码与您帐户中的共享密码不匹配。
#21005收据服务器目前不可用。
#21006此收据有效但订阅已过期。

更新



我的应用被拒绝,因为元数据没有明确说明有关自动续订订阅的一些信息。


在您的元数据中在iTunes Connect上(在您的应用程序说明中):您需要明确且明确地向用户披露有关您的自动续订订阅的以下
信息:




  • 出版物或服务的标题

  • 订阅期限(每个订阅期间的交付时间和/或数量)

  • 订阅价格和适当的每期价格

  • 付款将在确认购买时从iTunes帐户收取

  • 订阅会自动续订,除非在当前期间结束前至少24小时关闭自动续订。

  • 帐户将在24小时之前收取续订费用当前期间结束,并确定续订的费用

  • 订阅可由用户管理,购买后转到用户的帐户设置可以关闭自动续订

  • 在有效订阅期内不允许取消当前订阅

  • 您的隐私政策和使用条款的链接

  • 免费试用期的任何未使用部分(如果提供)将在用户购买该出版物的订阅时被没收。


UPDATE II



应用程序再次遭到拒绝。生产AppStore验证URL未验证订阅收据。我无法在沙盒中重现这个问题,我的应用程序完美无瑕。调试此问题的唯一方法是再次提交应用程序以供查看并查看服务器日志。



更新III



另一种拒绝。与此同时,Apple记录了另外两种状态:

 #21007此收据是沙盒收据,但已发送到生产服务部门进行验证。 
#21008此收据是生产收据,但已发送到沙盒服务进行验证。

在提交应用程序进行审核之前,不应将服务器切换为生产
收据验证网址。如果有人,则在验证时返回状态21007。



这次拒绝如下:


应用程序以非标准方式启动In App Purchase流程。我们已经包含以下详细信息以帮助
解释此问题,并希望您考虑修改并重新提交
您的申请。



iTunes用户名&在应用程序启动时会立即询问密码。有关更多
信息,请参阅随附的屏幕截图。


我不知道为什么会发生这种情况。密码对话框是否因为先前的事务正在恢复而弹出?或者它是否从应用程序商店请求产品信息时弹出?



更新IV



我在5次拒绝后得到了它。我的代码做了最明显的错误。人们应该确保在交付给应用程序时始终完成交易。



如果交易未完成,它们会被送回应用程序,事情变得奇怪错误。



首先需要先付款,如下所示:

  //付款
SKPayment * payment = [SKPayment paymentWithProductIdentifier:productIdentifier];
[[SKPaymentQueue defaultQueue] addPayment:payment];

然后应用程序将很快重新调整其活动状态,并在app delegate上调用此方法:

   - (void)applicationWillResignActive:(UIApplication *)application 

当应用程序处于非活动状态时,App Store会弹出其对话框。当应用程序再次变为活动时:

   - (void)applicationDidBecomeActive:(UIApplication *)application 

操作系统通过以下方式交付交易:

   - (void)paymentQueue:(SKPaymentQueue *)队列updatedTransactions:(NSArray *)交易
{

for(SKPaymentTransaction *交易中的交易)
{

switch(transaction.transactionState)
{
case SKPaymentTransactionStatePurchased:{
[self completeTransaction:transaction];
休息;
}
案例SKPaymentTransactionStateFailed:{
[self failedTransaction:transaction];
休息;
}
案例SKPaymentTransactionStateRestored:{
[self restoreTransaction:transaction];
休息;
}
默认值:
break;
}
}
}

然后一个完成交易:

  //新购买
- (无效)completeTransaction:(SKPaymentTransaction *)交易
{
[self recordTransaction:transaction];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

请参阅如何调用方法 finishTransaction 将收到的交易传递给 recordTransaction ,然后调用应用服务器并通过App Store进行订阅收据验证。像这样:

   - (void)recordTransaction:(SKPaymentTransaction *)交易
{
[self subscribeWithTransaction :交易];
}


- (void)subscribeWithTransaction:(SKPaymentTransaction *)transaction {

NSData * receiptData = [transaction transactionReceipt];
NSString * receiptEncoded = [Kriya base64encode:(uint8_t *)receiptData.bytes length:receiptData.length]; //在发送之前编码为base64

NSString * urlString = [NSString stringWithFormat:@ %@ / api /%@ /%@ / subscribe,[Kriya server_url],APP_ID,[Kriya deviceId]];

NSURL * url = [NSURL URLWithString:urlString];
ASIFormDataRequest * request = [[[ASIFormDataRequest alloc] initWithURL:url] autorelease];
[请求setPostValue:[[交易付款] productIdentifier] forKey:@product];
[请求setPostValue:receiptEncoded forKey:@receipt];
[请求setPostValue:[Kriya deviceModelString] forKey:@model];
[请求setPostValue:[Kriya deviceiOSString] forKey:@ios];
[请求setPostValue:[appDelegate version] forKey:@v];

[请求setDidFinishSelector:@selector(subscribeWithTransactionFinished :)];
[请求setDidFailSelector:@selector(subscribeWithTransactionFailed :)];
[request setDelegate:self];

[请求startAsynchronous];

}

以前我的代码试图调用 finishTransaction 仅在我的服务器验证收据后,但到那时交易已经以某种方式丢失了。所以请尽快确保 finishTransaction



另一个可能遇到的问题是事实,当应用程序位于沙箱中时,它会调用沙盒App Store验证网址,但是当它处于审核状态时,它会以某种方式介于世界之间。所以我不得不改变我的服务器代码:

  APPLE_SHARED_PASS =83f1ec5e7d864e89beef4d2402091cd0#你可以在iTunes Connect $ b中得到这个$ b APPLE_RECEIPT_VERIFY_URL_SANDBOX =https://sandbox.itunes.apple.com/verifyReceipt
APPLE_RECEIPT_VERIFY_URL_PRODUCTION =https://buy.itunes.apple.com/verifyReceipt

def self .verify_receipt_for(b64_receipt,receipt_verify_url)
json_resp = nil
url = URI.parse(receipt_verify_url)
http = Net :: HTTP.new(url.host,url.port)
http.use_ssl = true
http.verify_mode = OpenSSL :: SSL :: VERIFY_NONE
json_request = {'receipt-data'=> b64_receipt,'password'=> APPLE_SHARED_PASS} .to_json
resp,resp_body = http.post(url.path,json_request.to_s,{'Content-Type'=>'application / x-www-form-urlencoded'})
如果resp.code =='200'
json_resp = JSON.parse(resp_body)
end
json_resp
end

def self.verify_receipt(b64_receipt )
json_resp = Subscription.verify_receipt_for(b64_receipt,APPLE_RECEIPT_VERIFY_URL_PRODUCTION)
if json_resp!= nil
if json_resp.kind_of?哈希
如果json_resp ['status'] == 21007
#try沙箱然后
json_resp = Subscription.verify_receipt_for(b64_receipt,APPLE_RECEIPT_VERIFY_URL_SANDBOX)
end
end
结束
json_resp
结束

所以基本上总是通过生产验证URL,但如果它返回21007代码,则表示沙箱收据已发送到生产URL,然后只需使用沙箱URL再次尝试。这样你的应用程序在沙盒和生产模式下的工作方式相同。



最后Apple希望我在订阅按钮旁边添加一个RESTORE按钮来处理多个案例一个用户拥有的设备。然后,此按钮调用 [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; ,应用程序将通过已恢复的交易(如果有)进行交付。



此外,有时测试用户帐户会以某种方式受到污染,并且事情停止工作,订阅时可能会出现无法连接到iTunes商店消息。它有助于创建一个新的测试用户。



以下是相关代码的其余部分:

   - (void)restoreTransaction:(SKPaymentTransaction *)transaction 
{
[self recordTransaction:transaction];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

- (void)failedTransaction:(SKPaymentTransaction *)transaction
{
if(transaction.error.code == SKErrorPaymentCancelled)
{
//给用户带来错误
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];

}



祝你顺利InAppPurchase编程经验。 : - )


Apple finally introduced the so called auto-renewable subscriptions yesterday. Since I only have few (sandbox-only) experiences with in-app-purchase, I'm not sure that I got it all right here. It seems one needs a server side verification of the receipts. It seems the only way to find out if the subscription is still valid, is to store the original transaction data on server side. Apples programming guide with respect to this topic is all cryptic to me.

My expectation was, that I can work with an iOS client only, by just asking iTunes via store kit api did he/she already buy this (subscription-)product and receiving a yes/no answer together with an expiration date.

Does anyone have experiences with auto-renewable subscriptions or (because they seem somehow similar) non-consumable products? Are there any good tutorials about this?

Thank you.

解决方案

I have it running in the sandbox, almost going live...

One should use a server to verify the receipts.

On the server you can record the device udid with the receipt data, since receipts are always freshly generated, and it will work across multiple devices, since the receipts are always freshly generated.

On the device one does not need to store any sensitive data, and should not :)

One should check the last receipt with the store whenever the app comes up. The app calls the server and the server validates with the store. As long as the store returns a valid receipt app serves the feature.

I developed a Rails3.x app to handle the server side, the actual code for the verification looks like this:

APPLE_SHARED_PASS = "enter_yours"
APPLE_RECEIPT_VERIFY_URL = "https://sandbox.itunes.apple.com/verifyReceipt" #test
# APPLE_RECEIPT_VERIFY_URL = "https://buy.itunes.apple.com/verifyReceipt"     #real
def self.verify_receipt(b64_receipt)
  json_resp = nil
  url = URI.parse(APPLE_RECEIPT_VERIFY_URL)
  http = Net::HTTP.new(url.host, url.port)
  http.use_ssl = true
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  json_request = {'receipt-data' => b64_receipt, 'password' => APPLE_SHARED_PASS}.to_json
  resp, resp_body = http.post(url.path, json_request.to_s, {'Content-Type' => 'application/x-www-form-urlencoded'})
  if resp.code == '200'
    json_resp = JSON.parse(resp_body)
    logger.info "verify_receipt response: #{json_resp}"
  end
  json_resp
end
#App Store error responses
#21000 The App Store could not read the JSON object you provided.
#21002 The data in the receipt-data property was malformed.
#21003 The receipt could not be authenticated.
#21004 The shared secret you provided does not match the shared secret on file for your account.
#21005 The receipt server is not currently available.
#21006 This receipt is valid but the subscription has expired.

UPDATE

My app got rejected, because the meta data was not clearly stating some info about the auto-renewable subscriptions.

In your meta data at iTunes Connect (in your app description): You need to clearly and conspicuously disclose to users the following information regarding Your auto-renewing subscription:  

  • Title of publication or service
  • Length of subscription (time period and/or number of deliveries during each subscription period)
  • Price of subscription, and price per issue if appropriate
  • Payment will be charged to iTunes Account at confirmation of purchase
  • Subscription automatically renews unless auto-renew is turned off at least 24-hours before the end of the current period
  • Account will be charged for renewal within 24-hours prior to the end of the current period, and identify the cost of the renewal
  • Subscriptions may be managed by the user and auto-renewal may be turned off by going to the user’s Account Settings after purchase
  • No cancellation of the current subscription is allowed during active subscription period
  • Links to Your Privacy Policy and Terms of Use
  • Any unused portion of a free trial period, if offered, will be forfeited when the user purchases a subscription to that publication."

UPDATE II

App got rejected again. The subscription receipt is not verified by the production AppStore verify url. I can not reproduce this problem in the sandbox, my app works flawless. The only way to debug this, is to submit the app again for review and look at the server log.

UPDATE III

Another rejection. Meanwhile Apple documented two more statuses:

#21007 This receipt is a sandbox receipt, but it was sent to the production service for verification.
#21008 This receipt is a production receipt, but it was sent to the sandbox service for verification.

Before one submits the app for review, one should not switch the server to production receipt verify url. if one does, one gets status 21007 returned on verification.

This time the rejection reads like this:

Application initiates the In App Purchase process in a non-standard manner. We have included the following details to help explain the issue and hope you’ll consider revising and resubmitting your application.

iTunes username & password are being asked for immediately on application launch. Please refer to the attached screenshot for more information.

I have no clue why this happens. Does the password dialog pop up because a previous transaction is being restored? or does it pop up at the point of requesting products information from the app store?

UPDATE IV

I got it right after 5 rejections. My code was doing the most obvious error. One should really make sure to always finish transactions when they are delivered to the app.

If transactions aren't finished, they get delivered back to the app and things go strangely wrong.

One needs to initiate a payment first, like this:

//make the payment
SKPayment *payment = [SKPayment paymentWithProductIdentifier:productIdentifier];
[[SKPaymentQueue defaultQueue] addPayment:payment];

Then the app will shortly resign its active state and this method on the app delegate is called:

- (void)applicationWillResignActive:(UIApplication *)application

While the app is inactive, the App Store pops up its dialogs. as the app becomes active again:

- (void)applicationDidBecomeActive:(UIApplication *)application

The OS delivers the transaction through:

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{

  for (SKPaymentTransaction *transaction in transactions)
  {

    switch (transaction.transactionState)
    {
        case SKPaymentTransactionStatePurchased: {
            [self completeTransaction:transaction];
            break;
        }
        case SKPaymentTransactionStateFailed: {
            [self failedTransaction:transaction];
            break;
        }
        case SKPaymentTransactionStateRestored: {
            [self restoreTransaction:transaction];
            break;
        }
        default:
            break;
      }
  }
}

And then one completes the transaction:

//a fresh purchase
- (void) completeTransaction: (SKPaymentTransaction *)transaction
{
    [self recordTransaction: transaction];
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; 
}

See, how one calls the method finishTransaction right after passing the received transaction to recordTransaction, which then calls the apps server and does the subscription receipt verification with the App Store. Like this:

- (void)recordTransaction: (SKPaymentTransaction *)transaction 
{
    [self subscribeWithTransaction:transaction];
}


- (void)subscribeWithTransaction:(SKPaymentTransaction*)transaction {

    NSData *receiptData = [transaction transactionReceipt];
    NSString *receiptEncoded = [Kriya base64encode:(uint8_t*)receiptData.bytes length:receiptData.length];//encode to base64 before sending

    NSString *urlString = [NSString stringWithFormat:@"%@/api/%@/%@/subscribe", [Kriya server_url], APP_ID, [Kriya deviceId]];

    NSURL *url = [NSURL URLWithString:urlString];
    ASIFormDataRequest *request = [[[ASIFormDataRequest alloc] initWithURL:url] autorelease];
    [request setPostValue:[[transaction payment] productIdentifier] forKey:@"product"];
    [request setPostValue:receiptEncoded forKey:@"receipt"];
    [request setPostValue:[Kriya deviceModelString] forKey:@"model"];
    [request setPostValue:[Kriya deviceiOSString] forKey:@"ios"];
    [request setPostValue:[appDelegate version] forKey:@"v"];

    [request setDidFinishSelector:@selector(subscribeWithTransactionFinished:)];
    [request setDidFailSelector:@selector(subscribeWithTransactionFailed:)];
    [request setDelegate:self];

    [request startAsynchronous];

}

Previously my code was trying to call finishTransaction only after my server verified the receipt, but by then the transaction was somehow lost already. so just make sure to finishTransaction as soon as possible.

Also another problem one can run into is the fact, that when the app is in the sandbox it calls the sandbox App Store verification url, but when it is in review, it is somehow between worlds. So i had to change my server code like this:

APPLE_SHARED_PASS = "83f1ec5e7d864e89beef4d2402091cd0" #you can get this in iTunes Connect
APPLE_RECEIPT_VERIFY_URL_SANDBOX    = "https://sandbox.itunes.apple.com/verifyReceipt"
APPLE_RECEIPT_VERIFY_URL_PRODUCTION = "https://buy.itunes.apple.com/verifyReceipt"

  def self.verify_receipt_for(b64_receipt, receipt_verify_url)
    json_resp = nil
    url = URI.parse(receipt_verify_url)
    http = Net::HTTP.new(url.host, url.port)
    http.use_ssl = true
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    json_request = {'receipt-data' => b64_receipt, 'password' => APPLE_SHARED_PASS}.to_json
    resp, resp_body = http.post(url.path, json_request.to_s, {'Content-Type' => 'application/x-www-form-urlencoded'})
    if resp.code == '200'
      json_resp = JSON.parse(resp_body)
    end
    json_resp
end

def self.verify_receipt(b64_receipt)
    json_resp = Subscription.verify_receipt_for(b64_receipt, APPLE_RECEIPT_VERIFY_URL_PRODUCTION)
    if json_resp!=nil
      if json_resp.kind_of? Hash
        if json_resp['status']==21007 
          #try the sandbox then
          json_resp = Subscription.verify_receipt_for(b64_receipt, APPLE_RECEIPT_VERIFY_URL_SANDBOX)
        end
      end
    end
    json_resp
end

So basically one always verifies with the production URL, but if it returns 21007 code, then it means a sandbox receipt was sent to the production URL and then one simply tries again with the sandbox URL. This way your app works the same in sandbox and production mode.

And finally Apple wanted me to add a RESTORE button next to the subscription buttons, to handle the case of multiple devices owned by one user. This button then calls [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; and the app will be deliver with restored transactions (if any).

Also, sometimes the test user accounts get contaminated somehow and things stop working and you may get a "Can not connect to iTunes store" message when subscribing. It helps to create a new test user.

Here is the rest of the relevant code:

- (void) restoreTransaction: (SKPaymentTransaction *)transaction
{
    [self recordTransaction: transaction];
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; 
}

- (void) failedTransaction: (SKPaymentTransaction *)transaction
{
    if (transaction.error.code == SKErrorPaymentCancelled)
    {
        //present error to user here 
    }
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];    

}

I wish you a smooth InAppPurchase programming experience. :-)

这篇关于任何(早期)iOS自动续订订阅体验的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆