iphone - iOS 自动续订订阅的任何(早期)体验

标签 iphone ipad ios in-app-purchase

苹果终于推出了所谓的auto-renewable subscriptions昨天。由于我在应用内购买方面的经验很少(仅限沙盒),所以我不确定我在这里是否一切顺利。似乎需要对收据进行服务器端验证。找出订阅是否仍然有效的唯一方法似乎是将原始交易数据存储在服务器端。关于这个主题的苹果编程指南对我来说都是神秘的。

我的期望是,我只能使用 iOS 客户端,只需通过 store kit api 询问 iTunes,他/她是否已经购买了这个(订阅)产品并收到了是/否的答案以及到期日期。

有没有人有过自动更新订阅或(因为它们看起来有点相似)非消耗性产品的经验?有没有关于这个的好教程?

谢谢你。

最佳答案

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

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

在服务器上,您可以使用收据数据记录设备 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.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.

更新

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

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


更新二

应用再次被拒绝。订阅收据未通过生产 AppStore 验证 url 进行验证。我无法在沙盒中重现此问题,我的应用程序完美无缺。调试此问题的唯一方法是再次提交应用程序以供审核并查看服务器日志。

更新三

另一个拒绝。与此同时,Apple 记录了另外两种状态:
#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.

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

这次拒绝是这样的:

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.



我不知道为什么会这样。是否因为先前的交易正在恢复而弹出密码对话框?还是在从应用商店请求产品信息时弹出?

更新四

5次被拒后我就成功了。我的代码出现了最明显的错误。人们应该真正确保在交付到应用程序时始终完成交易。

如果交易没有完成,它们会被送回应用程序,然后事情就会出现奇怪的错误。

需要先发起付款,如下所示:
//make the payment
SKPayment *payment = [SKPayment paymentWithProductIdentifier:productIdentifier];
[[SKPaymentQueue defaultQueue] addPayment:payment];

然后应用程序将很快退出其事件状态,并调用应用程序委托(delegate)上的此方法:
- (void)applicationWillResignActive:(UIApplication *)application

当应用程序处于非事件状态时,App Store 会弹出其对话框。当应用再次激活时:
- (void)applicationDidBecomeActive:(UIApplication *)application

操作系统通过以下方式传递事务:
- (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;
      }
  }
}

然后完成交易:
//a fresh purchase
- (void) completeTransaction: (SKPaymentTransaction *)transaction
{
    [self recordTransaction: transaction];
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; 
}

看,如何调用方法 finishTransaction将收到的交易传递给 recordTransaction ,然后调用应用服务器并与 App Store 进行订阅回执验证。像这样:
- (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];

}

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

另一个可能遇到的问题是,当应用程序在沙盒中时,它会调用沙盒 App Store 验证 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

因此,基本上总是使用生产 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)
    {
        //present error to user here 
    }
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];    

}

祝您有一个流畅的 InAppPurchase 编程体验。 :-)

关于iphone - iOS 自动续订订阅的任何(早期)体验,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/5017731/

相关文章:

ios - 如何增加短信iPhone应用程序的UITextView大小和UIToolBar高度?

iphone - UISplitViewController 不会呈现模态视图 Controller

ios - 解码 AirPlay 镜像协议(protocol)

ios - SpriteKit - 仅半个屏幕生成随机 Sprite

javascript - 如何在 Cordova iOS 应用程序中打开 blob 内容的附件

ios - 与核心数据的多对多关系: how to populate intersection data?

iphone - 删除对象核心数据

iphone - MPMoviePlayer 完成按钮问题

iphone - 出现时放大网页 View

ios - 在 MDM 单一应用程序模式下禁用 iPad 的电源按钮