22

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?

ABCD
  • 7,914
  • 9
  • 54
  • 90

2 Answers2

35

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
ABCD
  • 7,914
  • 9
  • 54
  • 90
  • Great post. Upvoted both Q & A. However, apparently you aren't telling what do you do when you say:"The problem is that I forgot to explicitly close the transaction." I am facing same issue of download status never reaching to completion. Debugging on simulator though as I don't have real device with me at hand right now, just getting nervous. Can you throw some light? – Nirav Bhatt Feb 19 '13 at 19:24
  • I can't tell you whether this would work in simulator because I never tried, but at least in the earlier iOS, this wouldn't work. Not sure about the latest iOS. – ABCD Feb 20 '13 at 04:28
  • Q: Has your download even started? ie. Has it ever reached anything other than 0%? In my code, I have: NSLog(@"%f", download.progress);, please do the same and tell me what you have. – ABCD Feb 20 '13 at 04:30
  • In case your download has started (ie. 0%, 5%, 10% etc) but never finished. Have you checked for SKDownloadStateFinished? If you have, have you called finishTransaction? I have everything in the code. You must explicitly close a transaction once it's done otherwise next time Apple will try to download the same purchase again and you are screwed. – ABCD Feb 20 '13 at 04:34
  • 3
    It went beyond 75% but never reached 100%. Also never reached SKDownloadStateFinished but eventually failed. And yes I did finishTransaction after SKDownloadStateFinished, but as I said, it never gets hit. – Nirav Bhatt Feb 20 '13 at 05:55
  • This could be anything. You should definitely examine SKPaymentTransactionStateFailed in updatedTransactions. – ABCD Feb 20 '13 at 06:01
  • How to resume downloading? It seems it never auto resuming, for example, I stop the app when the downloading is about 75%. After I relaunch the app, it never resume downloading automatically? What should I do to resume downloading? – Bagusflyer Mar 31 '13 at 16:08
  • If the download is not getting to 100% check SKDownloadStateFailed on paymentQueue:updatedDownloads:. To resume previous downloads, it should resume automatically and hit your methods when you set the class as transaction Obsever, if not, try adding a button that calls [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]. – Juan de la Torre May 14 '13 at 05:10
  • Correct. Resuming is automatic if the previous transaction was not complete. – ABCD May 14 '13 at 05:54
  • Also noticed that on the simulator it doesn't pass 75%, testing on the device gives no problems. – Juan de la Torre May 14 '13 at 18:54
  • 1
    as in apple documentation:http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/StoreKitGuide/DevelopingwithStoreKit/DevelopingwithStoreKit.html Note: Store Kit can be tested in the iOS Simulator, except for hosted content downloads. – sefiroths Jul 25 '13 at 16:01
  • Please refer to this posting: [http://stackoverflow.com/questions/14080791/ios-downloading-from-apple-server-after-inapp-purchase](http://stackoverflow.com/questions/14080791/ios-downloading-from-apple-server-after-inapp-purchase). even in iOS 6.1 the simulator does not support context downloading. – Leslie Godwin Aug 01 '13 at 07:09
  • Thanks for a great post. It was really useful that you made the source code available on smallchess.com - looking at Utility.m helped me to understand how you're extracting the package contents. – DaveAlden Oct 02 '13 at 08:25
  • @Dpa99c could you please give us the link for the source code? – Samidjo Nov 02 '13 at 11:23
  • Note that SmallFish is another product to SmallChess and DOES NOT have in-app purchase. The actual app that has in-app purchase (SmallChess) is closed source. – ABCD Nov 03 '13 at 10:52
  • Has anyone got this to work? I'm stuck at 0% and it never goes to the SKDownloadStateFinished – lppier Nov 04 '13 at 06:10
  • I think you may need to test for download count for a given transaction in completeTransaction and restoreTransaction coz finishing the transaction can cause download process to terminate prematurely. – Benny Khoo Nov 12 '13 at 12:39
2

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.

GilesDMiddleton
  • 2,279
  • 22
  • 31
  • 1
    Apple's in-purchase code is not well implemented, everybody knows it. But some of your observations are in fact documented, for example, in (1) we use it to check the current progress. While the methods aren't documented well, using an external library just for it is a bit overkilled. – ABCD May 28 '15 at 11:48
  • Yes I use the updatedDownloads for monitoring progress if that's what you thought I meant. But I didn't expect updatedTransactions to be called multiple times for the same state of objects - which it appears to be. I accept I might be doing something wrong, but I think the overall advice to debug the hell out of these routines and develop aggressive test scenarios (like quit the app mid download/restore) is, I hope good advice! – GilesDMiddleton May 28 '15 at 11:53
  • I had the same frustation like you're having now back when I wrote this post a few years ago. I remember I sat and debugged for hours & hours. But you'll get used to it.... The method updatedTransactions is expected to be called multiple times, it's names as an "updated" method that means it'll be called whenever something happens. – ABCD May 28 '15 at 11:57
  • @GilesDMiddleton Can you clarify what you mean in your 4th point above. [Apple's StoreKitSuite sample code](https://developer.apple.com/library/content/samplecode/sc1991/Introduction/Intro.html) also starts the transaction's downloads where the state of the download is _waiting_: `case SKDownloadStateWaiting: [[SKPaymentQueue defaultQueue] startDownloads:@[download]]; break;` are you saying this is an error? – Aodh Jun 28 '17 at 00:10
  • 1
    @Aodh Maybe it's my bad use of double negatives. Point 4 is saying: Only call startDownload if state is Waiting or Paused. – GilesDMiddleton Jul 12 '17 at 12:04
  • @GilesDMiddleton Thanks! About starting the download when it's in the paused state..why not just call resume() ? – Aodh Jul 12 '17 at 15:38