In-App Purchase class


#1

I've created an InAppPurchase class for iOS and thought I'd share it here. Here's how it works:

1. To get your app set up and ready for In-App Purchase follow steps 1 - 13 from http://stackoverflow.com/a/19556337

2. In XCodes Capabilities turn on In-App Purchase

3. Copy the following .h and .mm files into your project:

InAppPurchase.h

​
#include "../../JuceLibraryCode/JuceHeader.h"

class InAppPurchaseListener;

class InAppPurchase
{
public:
    
    InAppPurchase();
    ~InAppPurchase();
    void addListener(InAppPurchaseListener* listener_) {listener = listener_;}
    void requestProduct(String productID);
    void purchaseProduct();
    void restoreProduct();
    
    String getLocalizedProductDescription();
    String getLocalizedProductTitle();
    String getProductPriceInLocalCurrency();
    
    void sendNotification(int notification, String message = String::empty, String secondaryMessage = String::empty);
    
    enum NotificationType {error, productReceived, purchasingProduct, productPurchased, productRestored};
    
private:
    
    InAppPurchaseListener* listener;
};

class InAppPurchaseListener
{
public:
    virtual ~InAppPurchaseListener() {}
    virtual void inAppPurchaseNotificationReceived(int notification, String message = String::empty, String secondaryMessage = String::empty) = 0;
};

InAppPurchase.mm

​
#include "InAppPurchase.h"
#import <StoreKit/StoreKit.h>

@interface InAppPurchaseWrapper : NSObject <SKProductsRequestDelegate,SKPaymentTransactionObserver>
{
    InAppPurchase* owner;
}

@property (strong, nonatomic) SKProduct* product;

@end

static InAppPurchaseWrapper *wrapper = nil;

@implementation InAppPurchaseWrapper

@synthesize product;

+ (id)sharedInstance
{
    if (wrapper == nil)
        wrapper = [[InAppPurchaseWrapper alloc] init];
        
    return wrapper;
}

- (void)assignOwner: (InAppPurchase*) owner_
{
    owner = owner_;
}

- (void) releaseProduct
{
    if (product)
    {
        [product release];
        product = nil;
    }
}

- (void) requestProduct: (NSString*) productID
{
    if([SKPaymentQueue canMakePayments])
    {
        NSLog(@"User can make payments");
        SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:productID]];
        productsRequest.delegate = self;
        [productsRequest start];
    }
    else
    {
        NSLog(@"User cannot make payments.");
        owner->sendNotification(InAppPurchase::error, "User cannot make payments.");
    }
}

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
    product = nil;
    NSUInteger count = [response.products count];
    
    if (count > 0)
    {
        product = [response.products objectAtIndex:0];
        [product retain];
        owner->sendNotification(InAppPurchase::productReceived);
    }
    else if (!product)
    {
        owner->sendNotification(InAppPurchase::error, "Product not valid.");
    }
}

- (void) purchase
{
    if (product)
    {
        SKPayment *payment = [SKPayment paymentWithProduct:product];
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    }
    else
        owner->sendNotification(InAppPurchase::error, "Product not avialable");
}

- (void) restore
{
    //this is called when the user restores purchases, you should hook this up to a button
    [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}

- (void) paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
    NSLog(@"received restored transactions: %lu", (unsigned long)queue.transactions.count);
    
    for (SKPaymentTransaction *transaction in queue.transactions)
    {
        if (SKPaymentTransactionStateRestored)
        {
            NSLog(@"Transaction state -> Restored");
            //called when the user successfully restores a purchase
            owner->sendNotification(InAppPurchase::productRestored);
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            break;
        }
    }
}

- (void) paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction *transaction in transactions)
    {
        int transactionState = transaction.transactionState;
        
        switch (transactionState)
        {
            case SKPaymentTransactionStatePurchasing: NSLog(@"Transaction state -> Purchasing");
                
                //called when the user is in the process of purchasing, do not add any of your own code here.
                owner->sendNotification(InAppPurchase::purchasingProduct);
                break;
                
            case SKPaymentTransactionStatePurchased:
                
                owner->sendNotification(InAppPurchase::productPurchased);
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                NSLog(@"Transaction state -> Purchased");
                break;
                
            case SKPaymentTransactionStateRestored:
                
                NSLog(@"Transaction state -> Restored");
                //add the same code as you did from SKPaymentTransactionStatePurchased here
                owner->sendNotification(InAppPurchase::productRestored);
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
                
            case SKPaymentTransactionStateFailed:
            {
                //called when the transaction does not finnish
                
                bool sendErrorNotification = true;
                String errorMessage = "Purchase Failed.";
                String secondaryMessage = String::empty;
                
                switch (transaction.error.code)
                {
                    case SKErrorUnknown:
                        break;
                        
                    case SKErrorClientInvalid:
                        secondaryMessage = "Client is invalid";
                        break;
                        
                    case SKErrorPaymentCancelled:
                        sendErrorNotification = false;
                        break;
                        
                    case SKErrorPaymentInvalid:
                        secondaryMessage = "Payment is invalid.";
                        break;
                        
                    case SKErrorPaymentNotAllowed:
                        secondaryMessage = "Payment is not allowed.";
                        break;
                        
                    case SKErrorStoreProductNotAvailable:
                        secondaryMessage = "Product is not available.";
                        break;
                        
                    default:
                        
                        break;
                }
                
                if (sendErrorNotification)
                    owner->sendNotification(InAppPurchase::error, errorMessage, secondaryMessage);
                
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
            }
        }
    }
}

- (NSString*) getLocalizedProductDescription
{
    NSString* desc = @"";
    
    if (product)
        desc = product.localizedDescription;
    
    return desc;
}

- (NSString*) getLocalizedProductTitle
{
    NSString* title = @"";
    
    if (product)
        title = product.localizedTitle;
    
    return title;
}

- (NSString*) getProductPriceInLocalCurrency
{
    NSString* price = @"";
    
    if (product)
    {
        NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
        [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
        [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
        [numberFormatter setLocale:product.priceLocale];
        price = [numberFormatter stringFromNumber:product.price];
    }
    
    return price;
}

@end

InAppPurchase::InAppPurchase()
{
    [[InAppPurchaseWrapper sharedInstance] assignOwner:this];
}

InAppPurchase::~InAppPurchase()
{
    [[InAppPurchaseWrapper sharedInstance] releaseProduct];
}

void InAppPurchase::requestProduct(String productID)
{
    NSString *nsProductID = (NSString*) productID.toCFString();
    [[InAppPurchaseWrapper sharedInstance] requestProduct:nsProductID];
}

void InAppPurchase::purchaseProduct()
{
    [[InAppPurchaseWrapper sharedInstance] purchase];
}

void InAppPurchase::restoreProduct()
{
    [[InAppPurchaseWrapper sharedInstance] restore];
}

String InAppPurchase::getLocalizedProductDescription()
{
    CFStringRef cfDesc = (CFStringRef) [[InAppPurchaseWrapper sharedInstance] getLocalizedProductDescription];
    return String::fromCFString(cfDesc);
}

String InAppPurchase::getLocalizedProductTitle()
{
    CFStringRef cfTitle = (CFStringRef) [[InAppPurchaseWrapper sharedInstance] getLocalizedProductTitle];
    return String::fromCFString(cfTitle);
}

String InAppPurchase::getProductPriceInLocalCurrency()
{
    CFStringRef cfPrice = (CFStringRef) [[InAppPurchaseWrapper sharedInstance] getProductPriceInLocalCurrency];
    return String::fromCFString(cfPrice);
}

void InAppPurchase::sendNotification(int notification, String message, String secondaryMessage)
{
    if (listener)
        listener->inAppPurchaseNotificationReceived(notification, message, secondaryMessage);
}

4. In your store component (or whatever class will be handling the in-app purchase process) do the following:

MyStoreUI.h

​
#include "InAppPurchase.h"

// inherit the listener
class MyStoreUI : public InAppPurchaseListener,
                  public Component
{
public:

    // your methods...
    
    void buttonClicked(Button* buttonThatWasClicked);

    // method for receiving notifications from the App Store
    void inAppPurchaseNotificationReceived(int notification, String message, String secondaryMessage);

private:

    // your InAppPurchase instance
    InAppPurchase inAppPurchase;
}

MyStoreUI.cpp

​
MyStoreUI::MyStoreUI()
{
    // your stuff

    inAppPurchase.addListener(this);
    inAppPurchase.requestProduct("YOUR PRODUCT ID THAT YOU SET UP IN STEP 1");
}

MyStoreUI::buttonClicked(Button* buttonThatWasClicked)
{
    if (buttonThatWasClicked == buyButton)
    {
        inAppPurchase.purchaseProduct();
    }
    else if (buttonThatWasClicked == restoreButton)
    {
        inAppPurchase.restoreProduct();
    }
}
void MyStoreUI::inAppPurchaseNotificationReceived(int notification, String message, String secondaryMessage)
{
    if (notification == InAppPurchase::error)
    {
        // handle error. There will be a message, possibly a secondaryMessage
    }
    else if (notification == InAppPurchase::productReceived)
    {
        // a result of the requestProduct method called above
        // good time to get your product info to display, using the following:
        // inAppPurchase.getLocalizedProductDescription();
        // inAppPurchase.getLocalizedProductTitle();
        // inAppPurchase.getProductPriceInLocalCurrency();
    }
    else if (notification == InAppPurchase::purchasingProduct)     
    {         
        // user hit "OK" on Apple's confirm pop up. Maybe a good time to show progress until productPurchased   
    else if (notification == InAppPurchase::productPurchased)
    {
        // transaction is complete. Give 'em the goods!
    }
    else if (notification == InAppPurchase::productRestored)
    {
        // handle product restore
    }
}

 

5. Go back to http://stackoverflow.com/a/19556337 and scroll down past his code, to the paragraph that starts with "Next, go into iTunesConnect..." to set up a test account, get the correct screenshot for submitting to the app store, and a few other things.

NOTE: I've tested everything on this except the restore process. My app doesn't use it. Also, this is for a single product, but it could probably be modified pretty easily to accomodate multiple products. And again this is for iOS, I'm not entirely sure if it can be used for a Mac app as is, or if that too would need modifications.

 


Any official IAP support?
#2

thanks for sharing this! I'm considering adding IAP soon so I'll be sure to bookmark this :)