diff --git a/MKStoreKit.h b/MKStoreKit.h index c21eca5..3dec9ec 100644 --- a/MKStoreKit.h +++ b/MKStoreKit.h @@ -102,10 +102,14 @@ extern NSString *const kMKStoreKitRestoringPurchasesFailedNotification; extern NSString *const kMKStoreKitReceiptValidationFailedNotification; /*! - * @abstract This notification is posted when MKStoreKit detects expiration of a auto-renewing subscription + * @abstract This notification is posted when MKStoreKit detects expiration of an auto-renewing subscription */ extern NSString *const kMKStoreKitSubscriptionExpiredNotification; +/*! + * @abstract This notification is posted when MKStoreKit updates the expiration date of an auto-renewing subscription + */ +extern NSString *const kMKStoreKitSubscriptionDateUpdatedNotification; /*! * @abstract The singleton class that takes care of In App Purchasing @@ -162,7 +166,7 @@ extern NSString *const kMKStoreKitSubscriptionExpiredNotification; * @abstract Refreshes the App Store receipt and prompts the user to authenticate. * * @discussion - * This method can generate a reciept while debugging your application. In a production + * This method can generate a receipt while debugging your application. In a production * environment this should only be used in an appropriate context because it will present * an App Store login alert to the user (without explanation). */ @@ -187,7 +191,7 @@ extern NSString *const kMKStoreKitSubscriptionExpiredNotification; * * @discussion * This method checks against the local store maintained by MKStoreKit when the app was originally purchased - * This method can be used to determine if a user should recieve a free upgrade. For example, apps transitioning + * This method can be used to determine if a user should receive a free upgrade. For example, apps transitioning * from a paid system to a freemium system can determine if users are "grandfathered-in" and exempt from extra * freemium purchases. * @@ -209,6 +213,31 @@ extern NSString *const kMKStoreKitSubscriptionExpiredNotification; */ - (BOOL)isProductPurchased:(NSString *)productId; +/*! + * @abstract Returns the duration of an auto-renewing subscription product + * + * @discussion + * This method reads the duration from MKStoreKitConfigs.plist + * + * @seealso + * -expiryDateForProduct + */ +- (NSInteger)subscriptionDurationForProduct:(NSString *)productId; + +/*! + * @abstract Returns a JSON formated receipt + * + * @discussion + * The receipt can be sent to a subscription server for validation before returning subscription data. + * It contains two items: + * receipt-data -> the base64 encoded receipt data and the shared secret + * password -> the shared secret for the App Store + * + * @seealso + * -expiryDateForProduct + */ +- (NSData *)receiptJSONData; + /*! * @abstract Checks the expiry date for the product identified by the given productId * diff --git a/MKStoreKit.m b/MKStoreKit.m index a39c591..01e1430 100644 --- a/MKStoreKit.m +++ b/MKStoreKit.m @@ -47,6 +47,7 @@ NSString *const kMKStoreKitRestoringPurchasesFailedNotification = @"com.mugunthkumar.mkstorekit.failedrestoringpurchases"; NSString *const kMKStoreKitReceiptValidationFailedNotification = @"com.mugunthkumar.mkstorekit.failedvalidatingreceipts"; NSString *const kMKStoreKitSubscriptionExpiredNotification = @"com.mugunthkumar.mkstorekit.subscriptionexpired"; +NSString *const kMKStoreKitSubscriptionDateUpdatedNotification = @"com.mugunthkumar.mkstorekit.subscriptiondateupdated"; NSString *const kSandboxServer = @"https://sandbox.itunes.apple.com/verifyReceipt"; NSString *const kLiveServer = @"https://buy.itunes.apple.com/verifyReceipt"; @@ -104,7 +105,7 @@ + (void)initialize { errorDictionary = @{@(21000) : @"The App Store could not read the JSON object you provided.", @(21002) : @"The data in the receipt-data property was malformed or missing.", @(21003) : @"The receipt could not be authenticated.", - @(21004) : @"The shared secret you provided does not match the shared secret on file for your accunt.", + @(21004) : @"The shared secret you provided does not match the shared secret on file for your account.", @(21005) : @"The receipt server is not currently available.", @(21006) : @"This receipt is valid but the subscription has expired.", @(21007) : @"This receipt is from the test environment.", @@ -157,9 +158,17 @@ - (BOOL)isProductPurchased:(NSString *)productId { return [self.purchaseRecord.allKeys containsObject:productId]; } +- (NSInteger)subscriptionDurationForProduct:(NSString *)productId { + NSDictionary *subscriptions = [MKStoreKit configs][@"Subscriptions"]; + NSNumber *duration = subscriptions[productId]; + if (nil == duration) return -1; + return [duration integerValue]; +} + -(NSDate*) expiryDateForProduct:(NSString*) productId { NSNumber *expiresDateMs = self.purchaseRecord[productId]; + if (nil == expiresDateMs || [[NSNull null] isEqual:expiresDateMs]) return nil; return [NSDate dateWithTimeIntervalSince1970:[expiresDateMs doubleValue] / 1000.0f]; } @@ -189,10 +198,12 @@ - (void)startProductRequest { NSMutableArray *productsArray = [NSMutableArray array]; NSArray *consumables = [[MKStoreKit configs][@"Consumables"] allKeys]; NSArray *others = [MKStoreKit configs][@"Others"]; - + NSArray *subscriptions = [[MKStoreKit configs][@"Subscriptions"] allKeys]; + [productsArray addObjectsFromArray:consumables]; [productsArray addObjectsFromArray:others]; - + [productsArray addObjectsFromArray:subscriptions]; + SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:productsArray]]; productsRequest.delegate = self; @@ -280,28 +291,26 @@ - (void)requestDidFinish:(SKRequest *)request { NSLog(@"App receipt exists. Preparing to validate and update local stores."); [self startValidatingReceiptsAndUpdateLocalStore]; } else { - NSLog(@"Receipt request completed but there is no receipt. The user may have refused to login, or the reciept is missing."); + NSLog(@"Receipt request completed but there is no receipt. The user may have refused to login, or the receipt is missing."); // Disable features of your app, but do not terminate the app } } } -- (void)startValidatingAppStoreReceiptWithCompletionHandler:(void (^)(NSArray *receipts, NSError *error)) completionHandler { +- (NSData *)receiptJSONData { NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; NSError *receiptError; BOOL isPresent = [receiptURL checkResourceIsReachableAndReturnError:&receiptError]; if (!isPresent) { - // No receipt - In App Purchase was never initiated - completionHandler(nil, nil); - return; + NSLog(@"No receipt in NSBundle.appStoreReceiptURL - In App Purchase was never initiated: %@", receiptError.localizedDescription); + return nil; } - + NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL]; if (!receiptData) { // Validation fails - NSLog(@"Receipt exists but there is no data available. Try refreshing the reciept payload and then checking again."); - completionHandler(nil, nil); - return; + NSLog(@"Receipt exists but there is no data available. Try refreshing the receipt payload and then checking again."); + return nil; } NSError *error; @@ -309,9 +318,19 @@ - (void)startValidatingAppStoreReceiptWithCompletionHandler:(void (^)(NSArray *r [receiptData base64EncodedStringWithOptions:0] forKey:@"receipt-data"]; NSString *sharedSecret = [MKStoreKit configs][@"SharedSecret"]; if (sharedSecret) requestContents[@"password"] = sharedSecret; - + NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents options:0 error:&error]; - + + return requestData; +} + +- (void)startValidatingAppStoreReceiptWithCompletionHandler:(void (^)(NSArray *receipts, NSError *error)) completionHandler { + NSData *requestData = [self receiptJSONData]; + if (nil == requestData) { + completionHandler(nil, nil); + return; + } + #ifdef DEBUG NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:kSandboxServer]]; #else @@ -365,18 +384,24 @@ - (void)startValidatingReceiptsAndUpdateLocalStore { __block BOOL purchaseRecordDirty = NO; [receipts enumerateObjectsUsingBlock:^(NSDictionary *receiptDictionary, NSUInteger idx, BOOL *stop) { NSString *productIdentifier = receiptDictionary[@"product_id"]; - NSNumber *expiresDateMs = receiptDictionary[@"expires_date_ms"]; NSNumber *previouslyStoredExpiresDateMs = self.purchaseRecord[productIdentifier]; - if (expiresDateMs && ![expiresDateMs isKindOfClass:[NSNull class]] && ![previouslyStoredExpiresDateMs isKindOfClass:[NSNull class]]) { - if ([expiresDateMs doubleValue] > [previouslyStoredExpiresDateMs doubleValue]) { - self.purchaseRecord[productIdentifier] = expiresDateMs; - purchaseRecordDirty = YES; + NSNumber *cenceledDateMs = receiptDictionary[@"cancellation_date_ms"]; + if (!cenceledDateMs || [cenceledDateMs isKindOfClass:[NSNull class]]) { // transaction not cancelled + NSNumber *expiresDateMs = receiptDictionary[@"expires_date_ms"]; + if (expiresDateMs && ![expiresDateMs isKindOfClass:[NSNull class]]) { // check if expiry date is later than the stored date + if ([previouslyStoredExpiresDateMs isKindOfClass:[NSNull class]] || [expiresDateMs doubleValue] > [previouslyStoredExpiresDateMs doubleValue]) { + self.purchaseRecord[productIdentifier] = expiresDateMs; + purchaseRecordDirty = YES; + } } } }]; - if (purchaseRecordDirty) [self savePurchaseRecord]; - + if (purchaseRecordDirty) { + [self savePurchaseRecord]; + [[NSNotificationCenter defaultCenter] postNotificationName:kMKStoreKitSubscriptionDateUpdatedNotification object:nil]; + } + [self.purchaseRecord enumerateKeysAndObjectsUsingBlock:^(NSString *productIdentifier, NSNumber *expiresDateMs, BOOL *stop) { if (![expiresDateMs isKindOfClass: [NSNull class]]) { if ([[NSDate date] timeIntervalSince1970] > [expiresDateMs doubleValue]) { @@ -439,7 +464,7 @@ - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)tran } [queue finishTransaction:transaction]; - + NSDictionary *availableConsumables = [MKStoreKit configs][@"Consumables"]; NSArray *consumables = [availableConsumables allKeys]; if ([consumables containsObject:transaction.payment.productIdentifier]) { @@ -457,6 +482,7 @@ - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)tran } [self savePurchaseRecord]; + [self startValidatingReceiptsAndUpdateLocalStore]; // to get subscription expiry date [[NSNotificationCenter defaultCenter] postNotificationName:kMKStoreKitProductPurchasedNotification object:transaction.payment.productIdentifier]; } diff --git a/README.mdown b/README.mdown index ab505b3..e6852b2 100644 --- a/README.mdown +++ b/README.mdown @@ -1,6 +1,6 @@ #MKStoreKit -An in-App Purchase framework for iOS 7.0+. **MKStoreKit** makes in-App Purchasing super simple by remembering your purchases, validating reciepts, and tracking virtual currencies (consumable purchases). Additionally, it keeps track of auto-renewable subscriptions and their expirationd dates. It couldn't be easier! +An in-App Purchase framework for iOS 7.0+. **MKStoreKit** makes in-App Purchasing super simple by remembering your purchases, validating receipts, and tracking virtual currencies (consumable purchases). Additionally, it keeps track of auto-renewable subscriptions and their expiration dates. It couldn't be easier! ## Compatibility See the table below for details on MKStoreKit's requirements and compatibility. @@ -9,7 +9,7 @@ See the table below for details on MKStoreKit's requirements and compatibility. |:-------:|:------:|:-----------------------:|:-----------:|:------------:|:------------:| | 6.0 | Beta 1 | MKStoreKit Version 6.0 | iOS 7.0 + | 10.10 + | YES | -MKStoreKit 6 is a **complete revamp** of the project and is not compatible with previous versions of MKStoreKit. Refactoring should however be fairly simple. See the *Backwards Compatibility* column to determine the earliest comaptible version with the current release. +MKStoreKit 6 is a **complete revamp** of the project and is not compatible with previous versions of MKStoreKit. Refactoring should however be fairly simple. See the *Backwards Compatibility* column to determine the earliest compatible version with the current release. *MKStoreKit 6 is still in early beta (see the Stage column). Use with caution* @@ -39,10 +39,11 @@ It's close to impossible to maintain a working sample code for MKStoreKit as iTu ###Config File Format MKStoreKit uses a config file, MKStoreKitConfigs.plist for managing your product identifiers. -The config file is a Plist dictionary containing three keys, "Consumables", "Others" and "SharedSecret" +The config file is a Plist dictionary containing three keys, "Consumables", "Subscriptions", "Others" and "SharedSecret" -Consumables is the key where you provide a list of consumables in your app that should be managed as a virtual currency. -Others is the key where you provide a list of in app purchasable products +Consumables is the key where you provide a list of consumables in your app that should be managed as a virtual currency. +Subscriptions is the key where you provide a list of subscriptions and their durations. +Others is the key where you provide a list of in app purchasable products. SharedSecret is the key where you provide the shared secret generated on your iTunesConnect account. Here is a sample [MKStoreKitConfigs.plist](https://gist.github.com/MugunthKumar/330fc38b542c96fcecc6) @@ -56,7 +57,7 @@ Initialization is as simple as calling [[MKStoreKit sharedKit] startProductRequest] ``` -A sample initialziation code that you can add to your application:didFinishLaunchingWithOptions: is below +A sample initialization code that you can add to your application:didFinishLaunchingWithOptions: is below ``` objective-c [[MKStoreKit sharedKit] startProductRequest];