From 3ea3fbb32fabaf9ee7d40c06f563f2efd5c4328e Mon Sep 17 00:00:00 2001 From: Yonat Sharon Date: Wed, 8 Apr 2015 22:58:42 +0300 Subject: [PATCH 1/8] added support for configuring durations of subscriptions in MKStoreKitConfigs.plist under @"Subscriptions" Dictionary, and later getting them using -subscriptionDurationForProduct: --- MKStoreKit.h | 11 +++++++++++ MKStoreKit.m | 13 +++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/MKStoreKit.h b/MKStoreKit.h index c21eca5..baa7839 100644 --- a/MKStoreKit.h +++ b/MKStoreKit.h @@ -209,6 +209,17 @@ 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 Checks the expiry date for the product identified by the given productId * diff --git a/MKStoreKit.m b/MKStoreKit.m index a39c591..04aa072 100644 --- a/MKStoreKit.m +++ b/MKStoreKit.m @@ -157,6 +157,13 @@ - (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]; @@ -189,10 +196,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; From 3da226a3efc79475414e664d454b359fcebd400a Mon Sep 17 00:00:00 2001 From: Yonat Sharon Date: Thu, 9 Apr 2015 11:25:15 +0300 Subject: [PATCH 2/8] Added Subscriptions to README --- README.mdown | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.mdown b/README.mdown index ab505b3..2dd8daf 100644 --- a/README.mdown +++ b/README.mdown @@ -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) From b011b545fa6bb6cc06c6470f42753bc2b64e3161 Mon Sep 17 00:00:00 2001 From: Yonat Sharon Date: Fri, 10 Apr 2015 22:56:23 +0300 Subject: [PATCH 3/8] don't crash if save expiry date is nil --- MKStoreKit.m | 1 + 1 file changed, 1 insertion(+) diff --git a/MKStoreKit.m b/MKStoreKit.m index 04aa072..710e3d4 100644 --- a/MKStoreKit.m +++ b/MKStoreKit.m @@ -167,6 +167,7 @@ - (NSInteger)subscriptionDurationForProduct:(NSString *)productId { -(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]; } From 452d79f26203a5823bea733a0a71b3c3a26fe1b2 Mon Sep 17 00:00:00 2001 From: Yonat Sharon Date: Mon, 13 Apr 2015 10:17:45 +0300 Subject: [PATCH 4/8] Refactored -receiptJSONData out of -startValidatingAppStoreReceiptWithCompletionHandler: in order to allow client to send receipt to server, so that the server can validate it before returning data. --- MKStoreKit.h | 14 ++++++++++++++ MKStoreKit.m | 26 +++++++++++++++++--------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/MKStoreKit.h b/MKStoreKit.h index baa7839..eea1618 100644 --- a/MKStoreKit.h +++ b/MKStoreKit.h @@ -220,6 +220,20 @@ extern NSString *const kMKStoreKitSubscriptionExpiredNotification; */ - (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 710e3d4..fdbe044 100644 --- a/MKStoreKit.m +++ b/MKStoreKit.m @@ -296,22 +296,20 @@ - (void)requestDidFinish:(SKRequest *)request { } } -- (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; + return nil; } NSError *error; @@ -319,9 +317,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 From b45baa5715bf63b4ac329de7d2ea4cf8ecce0ba2 Mon Sep 17 00:00:00 2001 From: Yonat Sharon Date: Wed, 15 Apr 2015 18:47:13 +0300 Subject: [PATCH 5/8] Bug fix: correctly save expiry date for auto-renewing subscriptions. --- MKStoreKit.m | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/MKStoreKit.m b/MKStoreKit.m index fdbe044..f5f1566 100644 --- a/MKStoreKit.m +++ b/MKStoreKit.m @@ -385,8 +385,8 @@ - (void)startValidatingReceiptsAndUpdateLocalStore { 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]) { + if (expiresDateMs && ![expiresDateMs isKindOfClass:[NSNull class]]) { + if ([previouslyStoredExpiresDateMs isKindOfClass:[NSNull class]] || [expiresDateMs doubleValue] > [previouslyStoredExpiresDateMs doubleValue]) { self.purchaseRecord[productIdentifier] = expiresDateMs; purchaseRecordDirty = YES; } @@ -457,7 +457,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]) { @@ -475,6 +475,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]; } From f8d372ea9a7205cc68466c351c7e590e030f887e Mon Sep 17 00:00:00 2001 From: Yonat Sharon Date: Sat, 18 Apr 2015 14:54:02 +0300 Subject: [PATCH 6/8] comments & spelling --- MKStoreKit.h | 4 ++-- MKStoreKit.m | 6 +++--- README.mdown | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/MKStoreKit.h b/MKStoreKit.h index eea1618..fb32ff0 100644 --- a/MKStoreKit.h +++ b/MKStoreKit.h @@ -162,7 +162,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 +187,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. * diff --git a/MKStoreKit.m b/MKStoreKit.m index f5f1566..c3e4d0d 100644 --- a/MKStoreKit.m +++ b/MKStoreKit.m @@ -104,7 +104,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.", @@ -290,7 +290,7 @@ - (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 } } @@ -308,7 +308,7 @@ - (NSData *)receiptJSONData { 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."); + NSLog(@"Receipt exists but there is no data available. Try refreshing the receipt payload and then checking again."); return nil; } diff --git a/README.mdown b/README.mdown index 2dd8daf..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* @@ -57,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]; From 7dda0e2e9a3b397593b9763713b1a25506913b67 Mon Sep 17 00:00:00 2001 From: Yonat Sharon Date: Sat, 18 Apr 2015 14:55:19 +0300 Subject: [PATCH 7/8] check for receipt cancellation --- MKStoreKit.m | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/MKStoreKit.m b/MKStoreKit.m index c3e4d0d..5c77bdd 100644 --- a/MKStoreKit.m +++ b/MKStoreKit.m @@ -383,12 +383,15 @@ - (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]]) { - if ([previouslyStoredExpiresDateMs isKindOfClass:[NSNull class]] || [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; + } } } }]; From 85cfc2e025d68646749cacba13438b27430003cc Mon Sep 17 00:00:00 2001 From: Yonat Sharon Date: Sun, 19 Apr 2015 08:35:24 +0300 Subject: [PATCH 8/8] notify on updating subscription expiry date, for clients that rely on that data. --- MKStoreKit.h | 6 +++++- MKStoreKit.m | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/MKStoreKit.h b/MKStoreKit.h index fb32ff0..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 diff --git a/MKStoreKit.m b/MKStoreKit.m index 5c77bdd..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"; @@ -396,8 +397,11 @@ - (void)startValidatingReceiptsAndUpdateLocalStore { } }]; - 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]) {