A lightweight iOS library for In-App Purchases.
RMStore adds blocks and notifications to StoreKit, plus receipt verification, content downloads and transaction persistence. All in one class without external dependencies. Purchasing a product is as simple as:
[[RMStore defaultStore] addPayment:productID success:^(SKPaymentTransaction *transaction) {
NSLog(@"Purchased!");
} failure:^(SKPaymentTransaction *transaction, NSError *error) {
NSLog(@"Something went wrong");
}];
Using CocoaPods:
pod 'RMStore', '~> 0.7'
Or add the files from the RMStore directory if you're doing it manually.
Check out the wiki for more options.
RMStore adds blocks to all asynchronous StoreKit operations.
NSSet *products = [NSSet setWithArray:@[@"fabulousIdol", @"rootBeer", @"rubberChicken"]];
[[RMStore defaultStore] requestProducts:products success:^(NSArray *products, NSArray *invalidProductIdentifiers) {
NSLog(@"Products loaded");
} failure:^(NSError *error) {
NSLog(@"Something went wrong");
}];
[[RMStore defaultStore] addPayment:@"waxLips" success:^(SKPaymentTransaction *transaction) {
NSLog(@"Product purchased");
} failure:^(SKPaymentTransaction *transaction, NSError *error) {
NSLog(@"Something went wrong");
}];
[[RMStore defaultStore] restoreTransactionsOnSuccess:^(NSArray *transactions){
NSLog(@"Transactions restored");
} failure:^(NSError *error) {
NSLog(@"Something went wrong");
}];
[[RMStore defaultStore] refreshReceiptOnSuccess:^{
NSLog(@"Receipt refreshed");
} failure:^(NSError *error) {
NSLog(@"Something went wrong");
}];
iOS 11 adds a new delegate method on SKPaymentTransactionObserver
:
@optional
// Sent when a user initiates an IAP buy from the App Store
- (BOOL)paymentQueue:(SKPaymentQueue *)queue shouldAddStorePayment:(SKPayment *)payment forProduct:(SKProduct *)product NS_SWIFT_NAME(paymentQueue(_:shouldAddStorePayment:for:)) NS_AVAILABLE_IOS(11_0);
From Apple Docs:
This delegate method is called when the user has started an in-app purchase in the App Store, and is continuing the transaction in your app. Specifically, if your app is already installed, the method is called automatically. If your app is not yet installed when the user starts the in-app purchase in the App Store, the user gets a notification when the app installation is complete. This method is called when the user taps the notification. Otherwise, if the user opens the app manually, this method is called only if the app is opened soon after the purchase was started.
RMStore supports this with a new handler, called like this:
[RMStore defaultStore].shouldAddStorePaymentHandler = ^BOOL(SKPayment *payment, SKProduct *product) {
[[RMStore defaultStore] addPayment:product.productIdentifier success:^(SKPaymentTransaction *transaction) {
NSLog(@"Purchased!");
} failure:^(SKPaymentTransaction *transaction, NSError *error) {
NSLog(@"Something went wrong");
}];
return NO;
};
To test this in sandbox mode, open this URL in Safari:
itms-services://?action=purchaseIntent&bundleId=com.example.app&productIdentifier=product_name
RMStore sends notifications of StoreKit related events and extends NSNotification
to provide relevant information. To receive them, implement the desired methods of the RMStoreObserver
protocol and add the observer to RMStore
.
[[RMStore defaultStore] addStoreObserver:self];
...
[[RMStore defaultStore] removeStoreObserver:self];
- (void)storeProductsRequestFailed:(NSNotification*)notification
{
NSError *error = notification.rm_storeError;
}
- (void)storeProductsRequestFinished:(NSNotification*)notification
{
NSArray *products = notification.rm_products;
NSArray *invalidProductIdentifiers = notification.rm_invalidProductIdentififers;
}
Payment transaction notifications are sent after a payment has been requested or for each restored transaction.
- (void)storePaymentTransactionFinished:(NSNotification*)notification
{
NSString *productIdentifier = notification.rm_productIdentifier;
SKPaymentTransaction *transaction = notification.rm_transaction;
}
- (void)storePaymentTransactionFailed:(NSNotification*)notification
{
NSError *error = notification.rm_storeError;
NSString *productIdentifier = notification.rm_productIdentifier;
SKPaymentTransaction *transaction = notification.rm_transaction;
}
// iOS 8+ only
- (void)storePaymentTransactionDeferred:(NSNotification*)notification
{
NSString *productIdentifier = notification.rm_productIdentifier;
SKPaymentTransaction *transaction = notification.rm_transaction;
}
- (void)storeRestoreTransactionsFailed:(NSNotification*)notification;
{
NSError *error = notification.rm_storeError;
}
- (void)storeRestoreTransactionsFinished:(NSNotification*)notification
{
NSArray *transactions = notification.rm_transactions;
}
For Apple-hosted and self-hosted downloads:
- (void)storeDownloadFailed:(NSNotification*)notification
{
SKDownload *download = notification.rm_storeDownload; // Apple-hosted only
NSString *productIdentifier = notification.rm_productIdentifier;
SKPaymentTransaction *transaction = notification.rm_transaction;
NSError *error = notification.rm_storeError;
}
- (void)storeDownloadFinished:(NSNotification*)notification;
{
SKDownload *download = notification.rm_storeDownload; // Apple-hosted only
NSString *productIdentifier = notification.rm_productIdentifier;
SKPaymentTransaction *transaction = notification.rm_transaction;
}
- (void)storeDownloadUpdated:(NSNotification*)notification
{
SKDownload *download = notification.rm_storeDownload; // Apple-hosted only
NSString *productIdentifier = notification.rm_productIdentifier;
SKPaymentTransaction *transaction = notification.rm_transaction;
float progress = notification.rm_downloadProgress;
}
Only for Apple-hosted downloads:
- (void)storeDownloadCanceled:(NSNotification*)notification
{
SKDownload *download = notification.rm_storeDownload;
NSString *productIdentifier = notification.rm_productIdentifier;
SKPaymentTransaction *transaction = notification.rm_transaction;
}
- (void)storeDownloadPaused:(NSNotification*)notification
{
SKDownload *download = notification.rm_storeDownload;
NSString *productIdentifier = notification.rm_productIdentifier;
SKPaymentTransaction *transaction = notification.rm_transaction;
}
- (void)storeRefreshReceiptFailed:(NSNotification*)notification;
{
NSError *error = notification.rm_storeError;
}
- (void)storeRefreshReceiptFinished:(NSNotification*)notification { }
RMStore doesn't perform receipt verification by default but provides reference implementations. You can implement your own custom verification or use the reference verifiers provided by the library.
Both options are outlined below. For more info, check out the wiki.
RMStore provides receipt verification via RMStoreAppReceiptVerifier
(for iOS 7 or higher) and RMStoreTransactionReceiptVerifier
(for iOS 6 or lower). To use any of them, add the corresponding files from RMStore/Optional into your project and set the verifier delegate (receiptVerifier
) at startup. For example:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
const BOOL iOS7OrHigher = floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_6_1;
_receiptVerifier = iOS7OrHigher ? [[RMStoreAppReceiptVerifier alloc] init] : [[RMStoreTransactionReceiptVerifier alloc] init];
[RMStore defaultStore].receiptVerifier = _receiptVerifier;
// Your code
return YES;
}
If security is a concern you might want to avoid using an open source verification logic, and provide your own custom verifier instead.
RMStore delegates receipt verification, enabling you to provide your own implementation using the RMStoreReceiptVerifier
protocol:
- (void)verifyTransaction:(SKPaymentTransaction*)transaction
success:(void (^)())successBlock
failure:(void (^)(NSError *error))failureBlock;
Call successBlock
if the receipt passes verification, and failureBlock
if it doesn't. If verification could not be completed (e.g., due to connection issues), then error
must be of code RMStoreErrorCodeUnableToCompleteVerification
to prevent RMStore to finish the transaction.
You will also need to set the receiptVerifier
delegate at startup, as indicated above.
RMStore automatically downloads Apple-hosted content and provides a delegate for a self-hosted content.
Downloadable content hosted by Apple (SKDownload
) will be automatically downloaded when purchasing o restoring a product. RMStore will notify observers of the download progress by calling storeDownloadUpdate:
and finally storeDownloadFinished:
. Additionally, RMStore notifies when downloads are paused, cancelled or have failed.
RMStore will notify that a transaction finished or failed only after all of its downloads have been processed. If you use blocks, they will called afterwards as well. The same applies to restoring transactions.
RMStore delegates the downloading of self-hosted content via the optional contentDownloader
delegate. You can provide your own implementation using the RMStoreContentDownloader
protocol:
- (void)downloadContentForTransaction:(SKPaymentTransaction*)transaction
success:(void (^)())successBlock
progress:(void (^)(float progress))progressBlock
failure:(void (^)(NSError *error))failureBlock;
Call successBlock
if the download is successful, failureBlock
if it isn't and progressBlock
to notify the download progress. RMStore will consider that a transaction has finished or failed only after the content downloader delegate has successfully or unsuccessfully downloaded its content.
RMStore delegates transaction persistence and provides two optional reference implementations for storing transactions in the Keychain or in NSUserDefaults
. You can implement your transaction, use the reference implementations provided by the library or, in the case of non-consumables and auto-renewable subscriptions, get the transactions directly from the receipt.
For more info, check out the wiki.
RMStore requires iOS 5.0 or above and ARC.
RMStore is in initial development and its public API should not be considered stable. Future enhancements will include:
Copyright 2013-2014 Robot Media SL
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.