Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Any (early) experiences with auto-renewable subscriptions for iOS

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.

like image 993
Kai Huppmann Avatar asked Feb 16 '11 14:02

Kai Huppmann


People also ask

Does Apple automatically renew subscriptions?

They automatically renew at the end of their duration until the user chooses to cancel. Subscriptions are available on iOS, iPadOS, macOS, watchOS, and tvOS.

What percentage does Apple take from subscriptions?

Apple App Store: 30 percent standard commission on apps and in-app purchases of digital goods and services; sales of physical products are exempt. Subscription commission falls to 15 percent after one year.


2 Answers

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

like image 137
manitu Avatar answered Sep 27 '22 16:09

manitu


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.

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.

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.

like image 25
lxt Avatar answered Sep 27 '22 16:09

lxt