iOS的自动更新订阅体验

时间:2023-01-12 21:52:29

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.

苹果公司昨天终于推出了所谓的自动更新订阅服务。由于我只有很少(仅在沙箱中)使用in-app-purchase的经验,所以我不确定我是否在这里完全理解了它。似乎需要对收据进行服务器端验证。确定订阅是否仍然有效的唯一方法似乎是将原始事务数据存储在服务器端。关于这个话题的苹果编程指南对我来说都是神秘的。

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.

我的期望是,我可以只和iOS客户一起工作,通过商店kit api问iTunes,他/她已经购买了这个(订阅-)产品,并且收到一个是/没有答案和一个截止日期。

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.

谢谢你!

4 个解决方案

#1


49  

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.

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

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:

我开发了一个Rails3。x app处理服务器端,验证的实际代码如下:

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:  

在iTunes Connect的元数据中(在你的应用描述中):你需要清楚地、明显地向用户披露以下关于你自动更新订阅的信息:

  • 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
  • 付款将被支付到iTunes账户以确认购买。
  • Subscription automatically renews unless auto-renew is turned off at least 24-hours before the end of the current period
  • 除非在当前周期结束前至少24小时自动更新,否则订阅将自动更新
  • Account will be charged for renewal within 24-hours prior to the end of the current period, and identify the cost of the renewal
  • 账户将在当前期限结束前24小时内进行续期,并确定续期的成本
  • 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.

应用程序被拒绝了。生产AppStore不验证订阅收据验证url。我无法在沙箱中重现这个问题,我的应用程序完美无瑕。唯一的调试方法是再次提交应用程序以进行检查并查看服务器日志。

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.

在提交应用程序检查之前,不应该将服务器切换到生产收据验证url。如果有人这样做了,就会得到状态21007的验证。

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.

iTunes用户名和密码将在应用程序启动时立即被请求。更多信息请参考附件截图

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?

我不知道为什么会这样。密码对话框是否因为先前的事务被恢复而弹出?或者它会在向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.

5次拒绝之后,我马上拿到了。我的代码做了最明显的错误。在交付应用程序时,应该确保始终完成事务。

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:

然后app会很快退出活动状态,app委托上的这个方法叫做:

- (void)applicationWillResignActive:(UIApplication *)application

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

当应用程序处于非活动状态时,应用程序商店会弹出对话框。当app再次活跃时:

- (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:

看看,如何在将接收到的事务传递给recordTransaction后立即调用方法finishTransaction,记录事务然后调用应用服务器并与应用商店进行订阅收据验证。是这样的:

- (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.

以前,我的代码只是在服务器验证收据之后才尝试调用finishTransaction,但是到那时,事务已经不知何故丢失了。所以要确保尽快完成交易。

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:

另一个可能遇到的问题是,当应用程序在沙箱中时,它会调用沙箱应用程序商店验证url,但当它在检查时,它会在不同的世界之间。所以我不得不像这样修改我的服务器代码:

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.

基本上我们总是用产品URL进行验证,但是如果它返回21007个代码,那么它意味着一个沙箱收据被发送到产品URL,然后我们再尝试使用沙箱URL。这样,你的应用程序在沙箱和生产模式中也是一样的。

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).

最后,苹果希望我在订阅按钮旁边添加一个恢复按钮,以处理一个用户拥有的多个设备的情况。这个按钮然后调用[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];该应用程序将通过恢复的事务(如果有的话)交付。

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.

此外,有时测试用户帐户被污染,事情停止工作,你可能会收到“无法连接iTunes商店”的消息,当订阅。它有助于创建一个新的测试用户。

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. :-)

我希望你有一个顺利的程序设计经验。:-)

#2


10  

To determine whether a user has a valid subscription you either have to a) validate the existing receipt as described in the doc you linked to, or b) have the user repurchase the subscription and get a response back from Apple.

要确定用户是否有一个有效的订阅,你需要a)验证你链接到的文档中描述的现有收据,或者b)让用户重新购买订阅并从苹果获得响应。

The latter doesn't need any server-side interaction on your end, but is wrong and liable to get you rejected, because you'll need to prompt the user to effectively 'repurchase' your product every time you want to verify their sub.

后者不需要你端上任何服务器端交互,但它是错误的,很可能会让你被拒绝,因为每次你想验证他们的子代时,你需要提示用户有效地“回购”你的产品。

So, the only option is - as Apple recommend - to store and then verify the store receipt.

因此,唯一的选择是——正如苹果所建议的那样——存储并验证商店收据。

Now, I suppose in theory you could save the store receipt on device, and verify it that way. However, I think you'd have to be crazy to do this, because the new verification system requires a shared secret which you'd have to bundle with the app itself (a really bad idea).

现在,我想从理论上讲,你可以在设备上保存存储收据,并以这种方式验证它。然而,我认为你一定是疯了才会这么做,因为新的验证系统需要一个共享的秘密,你必须把这个秘密和应用本身捆绑在一起(真是个坏主意)。

Which means the answer to your question "can I work with an iOS client only" is 'technically yes', but to do so would be highly ill advised due to a number of security issues. Fortunately, the server side architecture you need to build is very simple - just linking iTunes receipts with UDIDs of devices, and a simple API to communicate with them. If you're not able to work that out, I'm sure very soon existing third-party in app purchase helpers such as Urban Airship will add auto-renew subs to their products.

这意味着你的问题“我能只与iOS客户端合作吗?”的答案在技术上是“可以”,但由于许多安全问题,这样做是非常不明智的。幸运的是,您需要构建的服务器端架构非常简单——只需将iTunes收据与设备的UDIDs连接起来,并使用一个简单的API与它们通信。如果你不能解决这个问题,我相信很快就会有第三方应用程序购买助手,比如城市飞艇,会给他们的产品添加自动更新子。

Linking UDID and receipt works fine because when the user makes a purchase on another device, Apple automatically restores their previous purchases. So you can save the receipt again, this time tied to a new UDID.

连接UDID和receipt效果很好,因为当用户在其他设备上购买时,苹果会自动恢复之前的购买。所以您可以再次保存收据,这次绑定到一个新的UDID。

#3


2  

Maybe the auto-renewable sandbox purchase servers are down? Consumable/Non-Consumable/Subscription sandbox item purchases are working, but auto-renewable purchase returns this error:

也许自动更新的沙箱购买服务器坏了?消耗品/非消耗品/订阅沙箱物品购买是有效的,但是自动更新购买返回这个错误:

Error Domain=SKErrorDomain Code=0 "Cannot connect to iTunes Store" UserInfo=0x15b600 {NSLocalizedDescription=Cannot connect to iTunes Store}

错误域=SKErrorDomain代码=0“不能连接到iTunes Store”UserInfo=0x15b600 {NSLocalizedDescription=无法连接到iTunes Store}

#4


0  

there is no need to store it on the server. you can verify it local on the client. we are currently coding an auto renewable script

不需要将它存储在服务器上。您可以在客户端上本地验证它。我们目前正在编写一个自动可更新的脚本

but currently it seems, the servers are down or something. verification with apple servers doesn t work

但目前看来,服务器出故障了。与苹果服务器的验证不起作用

#1


49  

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.

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

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:

我开发了一个Rails3。x app处理服务器端,验证的实际代码如下:

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:  

在iTunes Connect的元数据中(在你的应用描述中):你需要清楚地、明显地向用户披露以下关于你自动更新订阅的信息:

  • 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
  • 付款将被支付到iTunes账户以确认购买。
  • Subscription automatically renews unless auto-renew is turned off at least 24-hours before the end of the current period
  • 除非在当前周期结束前至少24小时自动更新,否则订阅将自动更新
  • Account will be charged for renewal within 24-hours prior to the end of the current period, and identify the cost of the renewal
  • 账户将在当前期限结束前24小时内进行续期,并确定续期的成本
  • 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.

应用程序被拒绝了。生产AppStore不验证订阅收据验证url。我无法在沙箱中重现这个问题,我的应用程序完美无瑕。唯一的调试方法是再次提交应用程序以进行检查并查看服务器日志。

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.

在提交应用程序检查之前,不应该将服务器切换到生产收据验证url。如果有人这样做了,就会得到状态21007的验证。

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.

iTunes用户名和密码将在应用程序启动时立即被请求。更多信息请参考附件截图

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?

我不知道为什么会这样。密码对话框是否因为先前的事务被恢复而弹出?或者它会在向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.

5次拒绝之后,我马上拿到了。我的代码做了最明显的错误。在交付应用程序时,应该确保始终完成事务。

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:

然后app会很快退出活动状态,app委托上的这个方法叫做:

- (void)applicationWillResignActive:(UIApplication *)application

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

当应用程序处于非活动状态时,应用程序商店会弹出对话框。当app再次活跃时:

- (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:

看看,如何在将接收到的事务传递给recordTransaction后立即调用方法finishTransaction,记录事务然后调用应用服务器并与应用商店进行订阅收据验证。是这样的:

- (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.

以前,我的代码只是在服务器验证收据之后才尝试调用finishTransaction,但是到那时,事务已经不知何故丢失了。所以要确保尽快完成交易。

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:

另一个可能遇到的问题是,当应用程序在沙箱中时,它会调用沙箱应用程序商店验证url,但当它在检查时,它会在不同的世界之间。所以我不得不像这样修改我的服务器代码:

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.

基本上我们总是用产品URL进行验证,但是如果它返回21007个代码,那么它意味着一个沙箱收据被发送到产品URL,然后我们再尝试使用沙箱URL。这样,你的应用程序在沙箱和生产模式中也是一样的。

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).

最后,苹果希望我在订阅按钮旁边添加一个恢复按钮,以处理一个用户拥有的多个设备的情况。这个按钮然后调用[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];该应用程序将通过恢复的事务(如果有的话)交付。

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.

此外,有时测试用户帐户被污染,事情停止工作,你可能会收到“无法连接iTunes商店”的消息,当订阅。它有助于创建一个新的测试用户。

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. :-)

我希望你有一个顺利的程序设计经验。:-)

#2


10  

To determine whether a user has a valid subscription you either have to a) validate the existing receipt as described in the doc you linked to, or b) have the user repurchase the subscription and get a response back from Apple.

要确定用户是否有一个有效的订阅,你需要a)验证你链接到的文档中描述的现有收据,或者b)让用户重新购买订阅并从苹果获得响应。

The latter doesn't need any server-side interaction on your end, but is wrong and liable to get you rejected, because you'll need to prompt the user to effectively 'repurchase' your product every time you want to verify their sub.

后者不需要你端上任何服务器端交互,但它是错误的,很可能会让你被拒绝,因为每次你想验证他们的子代时,你需要提示用户有效地“回购”你的产品。

So, the only option is - as Apple recommend - to store and then verify the store receipt.

因此,唯一的选择是——正如苹果所建议的那样——存储并验证商店收据。

Now, I suppose in theory you could save the store receipt on device, and verify it that way. However, I think you'd have to be crazy to do this, because the new verification system requires a shared secret which you'd have to bundle with the app itself (a really bad idea).

现在,我想从理论上讲,你可以在设备上保存存储收据,并以这种方式验证它。然而,我认为你一定是疯了才会这么做,因为新的验证系统需要一个共享的秘密,你必须把这个秘密和应用本身捆绑在一起(真是个坏主意)。

Which means the answer to your question "can I work with an iOS client only" is 'technically yes', but to do so would be highly ill advised due to a number of security issues. Fortunately, the server side architecture you need to build is very simple - just linking iTunes receipts with UDIDs of devices, and a simple API to communicate with them. If you're not able to work that out, I'm sure very soon existing third-party in app purchase helpers such as Urban Airship will add auto-renew subs to their products.

这意味着你的问题“我能只与iOS客户端合作吗?”的答案在技术上是“可以”,但由于许多安全问题,这样做是非常不明智的。幸运的是,您需要构建的服务器端架构非常简单——只需将iTunes收据与设备的UDIDs连接起来,并使用一个简单的API与它们通信。如果你不能解决这个问题,我相信很快就会有第三方应用程序购买助手,比如城市飞艇,会给他们的产品添加自动更新子。

Linking UDID and receipt works fine because when the user makes a purchase on another device, Apple automatically restores their previous purchases. So you can save the receipt again, this time tied to a new UDID.

连接UDID和receipt效果很好,因为当用户在其他设备上购买时,苹果会自动恢复之前的购买。所以您可以再次保存收据,这次绑定到一个新的UDID。

#3


2  

Maybe the auto-renewable sandbox purchase servers are down? Consumable/Non-Consumable/Subscription sandbox item purchases are working, but auto-renewable purchase returns this error:

也许自动更新的沙箱购买服务器坏了?消耗品/非消耗品/订阅沙箱物品购买是有效的,但是自动更新购买返回这个错误:

Error Domain=SKErrorDomain Code=0 "Cannot connect to iTunes Store" UserInfo=0x15b600 {NSLocalizedDescription=Cannot connect to iTunes Store}

错误域=SKErrorDomain代码=0“不能连接到iTunes Store”UserInfo=0x15b600 {NSLocalizedDescription=无法连接到iTunes Store}

#4


0  

there is no need to store it on the server. you can verify it local on the client. we are currently coding an auto renewable script

不需要将它存储在服务器上。您可以在客户端上本地验证它。我们目前正在编写一个自动可更新的脚本

but currently it seems, the servers are down or something. verification with apple servers doesn t work

但目前看来,服务器出故障了。与苹果服务器的验证不起作用