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.