Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to download In-App hosted content?

Tags:

I followed http://www.raywenderlich.com/21081/introduction-to-in-app-purchases-in-ios-6-tutorial to set up Apple hosted In-App purchase. It lists the products. When I want to download the products from Apple, I do something like this

-(void) paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {     for (SKPaymentTransaction * transaction in transactions)     {         switch (transaction.transactionState)         {             case SKPaymentTransactionStatePurchased:             {                 [[SKPaymentQueue defaultQueue] startDownloads:transaction.downloads];      ....  }  -(void) paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray *)downloads {     NSLog(@"paymentQues");      for (SKDownload *download in downloads)     {         switch (download.downloadState)         {             case SKDownloadStateActive:             {                 NSLog(@"%f", download.progress); break;              }     ...  }  -(void) paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions {  } 

I started the download in updatedTransactions, then updatedDownloads is called by Apple with downloadState == Active. Then next, Apple calls removedTransaction without ever actually start the download. The download progress is always 0% and updatedDownloads is never called with downloadState == Finished.

I don't know why my download never started and why my transaction get removed before the download finishes. Anybody has a working sample?

like image 401
SmallChess Avatar asked Oct 21 '12 03:10

SmallChess


People also ask

How do I download in-app purchases on my iPhone?

Scroll all the way to the bottom, tap Account, then tap Purchased. If you use Family Sharing, tap My Purchases or choose a family member's name to see content that they purchased. Find the app that you want to download, then tap the download button .

How do I purchase in-app purchases?

On the Google Play Store app, in-app purchases will be by the Price or Install button. On play.google.com/store, you'll see "Offers in-app purchases" below the name of the app.

Where are iOS apps hosted?

Apple Cloud Kit is Apple's cloud solution and another option worth considering for hosting iOS applications. It takes advantage of the iCloud to power up Apple platform applications. Cloud Kit is a feature-rich API (application programming interface) to help users store and query data within the cloud.

Why are in-app purchases not allowed?

If you get hit with a message on your Apple iPhone or iPad that says “Purchase – In-app purchases are not allowed” when trying to buy purchases from within apps, it may be related to a restriction setting on the device. From the Home screen, swipe over to the screen with the “Settings” icon, then select it.


2 Answers

The problem is that I forgot to explicitly close the transaction. For reference my full code is as follows. It has other things such as displaying a progress bar while downloading, but it's 100% working. Don't worry about Utility.h, it just defines some macros such as SAFE_RELEASE_VIEW.

Essentially I extended the sample in raywenderlich by defining two methods buy and download.

Pay close attention to updatedDownloads. Once the download finishes, I copy the contents to the user's document directory. When you download from Apple, the directory you have is like this:

    • ContentInfo.plist
      • Contents
        • Your Files

Apple only gives you the path to the Download Folder. You use the path to read ContentInfo.plist. In my app, I have a property "Files" in ContentInfo.plist which lists my files in the Contents folder. I then copy the files to the Documents folder. If you don't do this, you must guess which files you have in your Contents folder, or you simply copy everything inside.

This is the actual in-app purchase code for SmallChess (http://www.smallchess.com).

#import <StoreKit/StoreKit.h> #import "MBProgressHUD/MBProgressHUD.h" #import "Others/Utility.h" #import "Store/OnlineStore.h"  NSString *const ProductPurchasedNotification = @"ProductPurchasedNotification";  @implementation StoreTransaction @synthesize productID, payment; @end  @interface OnlineStore () <SKProductsRequestDelegate, SKPaymentTransactionObserver, MBProgressHUDDelegate> @end  @implementation OnlineStore {     NSSet *_productIDs;     MBProgressHUD *_progress;     NSMutableSet * _purchasedIDs;     SKProductsRequest * _productsRequest;     RequestProductsCompletionHandler _completionHandler; }  -(id) init {     if ([SKPaymentQueue canMakePayments] && (self = [super init]))     {         [[SKPaymentQueue defaultQueue] addTransactionObserver:self];     }      return self; }  #pragma mark MBProgressHUDDelegate  -(void) hudWasHidden:(MBProgressHUD *)hud {     NSAssert(_progress, @"ddd");      [_progress removeFromSuperview];          SAFE_RELEASE_VIEW(_progress); }  #pragma end  #pragma mark SKProductsRequestDelegate  -(void) request:(NSSet *)productIDs handler:(RequestProductsCompletionHandler)handler {         _completionHandler = [handler copy];      _productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIDs];     _productsRequest.delegate = self;     [_productsRequest start];     }  -(void) productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {     _productsRequest = nil;     _completionHandler(YES, response.products);     _completionHandler = nil; }  -(void) request:(SKRequest *)request didFailWithError:(NSError *)error {     NSLog(@"Failed to load list of products.");     _productsRequest = nil;      _completionHandler(NO, nil);     _completionHandler = nil; }  #pragma end  #pragma mark Transaction  -(void) provideContentForProduct:(SKPaymentTransaction *)payment productID:(NSString *)productID {     [_purchasedIDs addObject:productID];      [[NSUserDefaults standardUserDefaults] setBool:YES forKey:productID];     [[NSUserDefaults standardUserDefaults] synchronize];      StoreTransaction *transaction = [[StoreTransaction alloc] init];      [transaction setPayment:payment];     [transaction setProductID:productID];      [[NSNotificationCenter defaultCenter] postNotificationName:ProductPurchasedNotification object:transaction userInfo:nil]; }  -(void) completeTransaction:(SKPaymentTransaction *)transaction { #ifdef DEBUG     NSLog(@"completeTransaction"); #endif      [self provideContentForProduct:transaction productID:transaction.payment.productIdentifier];     [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; }  -(void) restoreTransaction:(SKPaymentTransaction *)transaction { #ifdef DEBUG     NSLog(@"restoreTransaction"); #endif      [self provideContentForProduct:transaction productID:transaction.originalTransaction.payment.productIdentifier];     [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; }  -(void) failedTransaction:(SKPaymentTransaction *)transaction { #ifdef DEBUG     NSLog(@"failedTransaction"); #endif      if (transaction.error.code != SKErrorPaymentCancelled)     {         NSLog(@"Transaction error: %@", transaction.error.localizedDescription);     }      [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; }  -(void) restoreCompletedTransactions { #ifdef DEBUG     NSLog(@"restoreCompletedTransactions"); #endif      [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; }  #pragma end  #pragma mark Buy & Download  -(BOOL) purchased:(NSString *)productID {     return [_purchasedIDs containsObject:productID]; }  -(void) buy:(SKProduct *)product {     SKPayment * payment = [SKPayment paymentWithProduct:product];     [[SKPaymentQueue defaultQueue] addPayment:payment]; }  -(void) download:(StoreTransaction *)transaction {     NSAssert(transaction.payment.transactionState == SKPaymentTransactionStatePurchased ||              transaction.payment.transactionState == SKPaymentTransactionStateRestored, @"The payment transaction must be completed");      if ([transaction.payment.downloads count])     {         [[SKPaymentQueue defaultQueue] startDownloads:transaction.payment.downloads];     } }  #pragma end  #pragma mark SKPaymentTransactionObserver  -(void) paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {     NSLog(@"RestoreCompletedTransactions"); }  -(void) paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {     for (SKPaymentTransaction * transaction in transactions)     {         switch (transaction.transactionState)         {             case SKPaymentTransactionStatePurchased:             { #ifdef DEBUG                 NSLog(@"SKPaymentTransactionStatePurchased"); #endif                  [[SKPaymentQueue defaultQueue] startDownloads:transaction.downloads];                 break;             }              case SKPaymentTransactionStateFailed:             {                 NSLog(@"Failed");                 [self failedTransaction:transaction];                 break;             }              case SKPaymentTransactionStateRestored:             {                  NSLog(@"Restored");                 [self restoreTransaction:transaction]; break;             }              case SKPaymentTransactionStatePurchasing:             { #ifdef DEBUG                 NSLog(@"SKPaymentTransactionStatePurchasing"); #endif                  break;             }         }     } }  -(void) paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error { #ifdef DEBUG     NSLog(@"restoreCompletedTransactionsFailedWithError"); #endif }  -(void) paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions { #ifdef DEBUG     NSLog(@"removedTransactions"); #endif }  -(void) paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray *)downloads {     for (SKDownload *download in downloads)     {         switch (download.downloadState)         {             case SKDownloadStateActive:             { #ifdef DEBUG                 NSLog(@"%f", download.progress);                 NSLog(@"%f remaining", download.timeRemaining); #endif                  if (download.progress == 0.0 && !_progress)                 {                     #define WAIT_TOO_LONG_SECONDS 60                     #define TOO_LARGE_DOWNLOAD_BYTES 4194304                      const BOOL instantDownload = (download.timeRemaining != SKDownloadTimeRemainingUnknown && download.timeRemaining < WAIT_TOO_LONG_SECONDS) ||                                                  (download.contentLength < TOO_LARGE_DOWNLOAD_BYTES);                      if (instantDownload)                     {                         UIView *window= [[UIApplication sharedApplication] keyWindow];                          _progress = [[MBProgressHUD alloc] initWithView:[[UIApplication sharedApplication] keyWindow]];                         [window addSubview:_progress];                          [_progress show:YES];                         [_progress setDelegate:self];                         [_progress setDimBackground:YES];                         [_progress setLabelText:@"Downloading"];                         [_progress setMode:MBProgressHUDModeAnnularDeterminate];                     }                     else                     {                         NSLog(@"Implement me!");                     }                 }                  [_progress setProgress:download.progress];                  break;             }              case SKDownloadStateCancelled: { break; }             case SKDownloadStateFailed:             {                 [Utility showAlert:@"Download Failed"                            message:@"Failed to download. Please retry later"                        cancelTitle:@"OK"                         otherTitle:nil                           delegate:nil];                 break;             }              case SKDownloadStateFinished:             {                 NSString *source = [download.contentURL relativePath];                 NSDictionary *dict = [[NSMutableDictionary alloc] initWithContentsOfFile:[source stringByAppendingPathComponent:@"ContentInfo.plist"]];                  if (![dict objectForKey:@"Files"])                 {                     [[SKPaymentQueue defaultQueue] finishTransaction:download.transaction];                     return;                 }                  NSAssert([dict objectForKey:@"Files"], @"The Files property must be valid");                  for (NSString *file in [dict objectForKey:@"Files"])                 {                     NSString *content = [[source stringByAppendingPathComponent:@"Contents"] stringByAppendingPathComponent:file];                      NSAssert([Utility isFileExist:content], @"Content path must be valid");                      // Copy the content to the Documents folder, don't bother with creating a directory for it                     DEFINE_BOOL(succeed, [Utility copy:content dst:[[Utility getDocPath] stringByAppendingPathComponent:file]]);                      NSAssert(succeed, @"Failed to copy the content");  #ifdef DEBUG                     NSLog(@"Copied %@ to %@", content, [[Utility getDocPath] stringByAppendingPathComponent:file]); #endif                 }                  if (download.transaction.transactionState == SKPaymentTransactionStatePurchased && _progress)                 {                     [Utility showAlert:@"Purchased Complete"                                message:@"Your purchase has been completed. Please refer to the FAQ if you have any questions"                            cancelTitle:@"OK"                             otherTitle:nil                               delegate:nil];                 }                  [_progress setDimBackground:NO];                 [_progress hide:YES];                  [[SKPaymentQueue defaultQueue] finishTransaction:download.transaction];                 break;             }              case SKDownloadStatePaused:             { #ifdef DEBUG                 NSLog(@"SKDownloadStatePaused"); #endif                 break;             }              case SKDownloadStateWaiting:             { #ifdef DEBUG                 NSLog(@"SKDownloadStateWaiting"); #endif                 break;             }         }     } }  #pragma end  @end 
like image 124
SmallChess Avatar answered Oct 26 '22 07:10

SmallChess


Accepting this isn't the answer to your particular problem,

I've experienced a few other problems in this area

  1. This method is called multiple times during the restore/purchase cycle, not just once.
  2. It can also be called on restarting the app without your invocation (to continue interrupted restores/downloads).
  3. If you only want to restore one productIdentifier, you have to filter out all the others.
  4. You shouldn't call startDownloads if the downloads state is not Waiting/Paused
  5. Sometimes, if you call startDownloads, you may get ANOTHER call to updatedTransaction for the SAME transaction and it's download.downloadState will still be Waiting - and if you call startDownloads a second time you can get the download progress/complete notification twice. This can cause intermittent problems if your downloadSuccess handler cleans the target location before copying files. Because the second download doesn't actually download to caches, so you have nothing to copy on the second notification. I've worked around this by recording a local array of downloads I know I've called startDownloads on, and fully completed.

I've put extensive debug statements into my store handler and each case statement to prove this behaviour, and made sure I'm not calling the queue more than once. I suggest anyone else starting out does the same - put in diagnostics.

It's not helped by the fact that SKDownload, SKPaymentTransaction do not have adequate description methods, you'll have to roll your own.

Alternatively use someone else's store handler on github.

like image 39
GilesDMiddleton Avatar answered Oct 26 '22 07:10

GilesDMiddleton