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.
