From 79a297d1a9d2c0c4f2354c1fae97c6849b609e67 Mon Sep 17 00:00:00 2001 From: Gautier Pelloux-Prayer Date: Fri, 29 May 2015 10:33:12 +0200 Subject: [PATCH] InAppProductsManager: add some comments and add account activation feature --- Classes/InAppProductsManager.h | 16 +++- Classes/InAppProductsManager.m | 148 +++++++++++++++++++++------------ Classes/WizardViewController.m | 2 +- 3 files changed, 109 insertions(+), 57 deletions(-) diff --git a/Classes/InAppProductsManager.h b/Classes/InAppProductsManager.h index b973336cd..f23a9cdeb 100644 --- a/Classes/InAppProductsManager.h +++ b/Classes/InAppProductsManager.h @@ -46,7 +46,14 @@ typedef NSString* IAPPurchaseNotificationStatus; //paid_account_id=test.autorenew_7days //receipt_validation_url=https://www.linphone.org/inapp.php //products_list=test.autorenew_7days -// Note: in Sandbox mode (test), autorenewal expire time is speed up (see http://stackoverflow.com/questions/8815271/what-expiry-date-should-i-see-for-in-app-purchase-in-the-application-sandbox) so that 7 days renewal is only 3 minutes! +// Note: in Sandbox mode (test), autorenewal expire time is speed up (see http://stackoverflow.com/questions/8815271/what-expiry-date-should-i-see-for-in-app-purchase-in-the-application-sandbox) so that 7 days renewal is only 3 minutes and: +//1 week = 3 minutes +//1 month = 5 minutes +//2 months = 10 minutes +//3 months = 15 minutes +//6 months = 30 minutes +//1 year = 1 hour + @interface InAppProductsManager : NSObject { NSString *latestReceiptMD5; @@ -64,11 +71,16 @@ typedef NSString* IAPPurchaseNotificationStatus; // TRUE if manager is available for usage - will be FALSE if an operation is already in progress or if not initialized or not enabled @property (readonly) BOOL available; +// TRUE if accountActivate was started but we did not receive response from server yet +@property (readonly) BOOL accountActivationInProgress; + - (BOOL)isPurchasedWithID:(NSString*)productId; // Purchase an account. You should not use this if manager is not available yet. -- (BOOL)purchaseAccount:(NSString *)phoneNumber withPassword:(NSString *)password andEmail:(NSString*)email; +- (BOOL)purchaseAccount:(NSString *)phoneNumber withPassword:(NSString *)password andEmail:(NSString*)email monthly:(BOOL)monthly; // Purchase a product. You should not use this if manager is not available yet. - (BOOL)purchaseWitID:(NSString *)productID; +// Activate purchased account. +- (BOOL)activateAccount:(NSString *)phoneNumber; // restore user purchases. You should not use this if manager is not available yet. Must be at a user action ONLY. - (BOOL)restore; diff --git a/Classes/InAppProductsManager.m b/Classes/InAppProductsManager.m index 316e78d7a..a9c35d7ac 100644 --- a/Classes/InAppProductsManager.m +++ b/Classes/InAppProductsManager.m @@ -32,9 +32,10 @@ #import "InAppProductsViewController.h" @interface InAppProductsManager() - @property (retain, nonatomic) NSDictionary *accountCreationData; - // needed because request:didFailWithError method is already used by SKProductsRequestDelegate... - @property (nonatomic, retain) InAppProductsXMLRPCDelegate *xmlrpc; +@property (retain, nonatomic) NSDate *expirationDate; +@property (retain, nonatomic) NSDictionary *accountCreationData; +// needed because request:didFailWithError method is already used by SKProductsRequestDelegate... +@property (nonatomic, retain) InAppProductsXMLRPCDelegate *xmlrpc; @end @implementation InAppProductsManager @@ -47,7 +48,7 @@ _enabled = (([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0) && ([SKPaymentQueue canMakePayments]) && ([[LinphoneManager instance] lpConfigBoolForKey:@"enabled" forSection:@"in_app_purchase"])); _initialized = false; _available = false; - + _accountActivationInProgress = false; if (_enabled) { self.xmlrpc = [[InAppProductsXMLRPCDelegate alloc] init]; _status = kIAPNotReady; @@ -64,7 +65,9 @@ if (!_enabled) return FALSE; for (NSString *prod in _productsIDPurchased) { - if ([prod isEqual: productID]) { + NSDate *now = [[[NSDate alloc] init] autorelease]; + // since multiple ID represent the same product, we must not check it + if (/*[prod isEqual: productID] &&*/[self.expirationDate earlierDate:now] == now) { bool isBought = true; LOGE(@"%@ is %s bought.", prod, isBought?"":"NOT"); return isBought; @@ -75,7 +78,7 @@ - (BOOL)purchaseWitID:(NSString *)productID { if (!_enabled||!_initialized||!_available) { - NSDictionary* dict = @{@"product_id":productID, @"error_msg": NSLocalizedString(@"In apps not ready yet", nil)}; + NSDictionary* dict = @{@"product_id":productID, @"error_msg": NSLocalizedString(@"Cannot purchase anything yet, please try again later.", nil)}; [self postNotificationforStatus:kIAPPurchaseFailed withDict:dict]; return FALSE; } @@ -95,9 +98,9 @@ } } -- (BOOL)purchaseAccount:(NSString *)phoneNumber withPassword:(NSString *)password andEmail:(NSString*)email { +- (BOOL)purchaseAccount:(NSString *)phoneNumber withPassword:(NSString *)password andEmail:(NSString*)email monthly:(BOOL)monthly { if (phoneNumber) { - NSString* productID = [[LinphoneManager instance] lpConfigStringForKey:@"paid_account_id" forSection:@"in_app_purchase"]; + NSString* productID = [[LinphoneManager instance] lpConfigStringForKey:(monthly?@"paid_account_id_monthly":@"paid_account_id") forSection:@"in_app_purchase"]; self.accountCreationData = @{ @"phoneNumber":[phoneNumber retain], @"password":[password retain], @"email":[email retain] }; if (![self purchaseWitID:productID]) { @@ -108,6 +111,34 @@ return false; } +- (BOOL)activateAccount:(NSString *)phoneNumber { + if (phoneNumber) { + NSString *receiptBase64 = [self getReceipt]; + if (receiptBase64) { + NSURL *URL = [NSURL URLWithString:[[LinphoneManager instance] lpConfigStringForKey:@"receipt_validation_url" forSection:@"in_app_purchase"]]; + XMLRPCRequest *request = [[XMLRPCRequest alloc] initWithURL: URL]; + //buying for the first time: need to create the account + //if ([transaction.transactionIdentifier isEqualToString:transaction.originalTransaction.transactionIdentifier]) { + [request setMethod: @"activate_account" withParameters:[NSArray arrayWithObjects: + @"", + phoneNumber, + receiptBase64, + @"", + @"apple", + nil]]; + _accountActivationInProgress = YES; + XMLRPCConnectionManager *manager = [XMLRPCConnectionManager sharedManager]; + [manager spawnConnectionWithXMLRPCRequest: request delegate: self.xmlrpc]; + LOGI(@"XMLRPC query %@", [request method]); + [request release]; + return true; + } else { + LOGE(@"Trying to activate account but no receipt available yet (probably doing it too soon)"); + } + } + return false; +} + -(BOOL)restore { if (!_enabled||!_initialized||!_available) { NSDictionary* dict = @{@"error_msg": NSLocalizedString(@"In apps not ready yet", nil)}; @@ -197,70 +228,66 @@ } } -- (void)validateReceipt: (SKPaymentTransaction*)transaction { - NSString *receiptBase64 = nil; +- (NSString*) getReceipt { NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; - // Test whether the receipt is present at the above URL if(![[NSFileManager defaultManager] fileExistsAtPath:[receiptURL path]]) { // We are probably in sandbox environment, trying to retrieve it... + return nil; + } + + NSString *receiptBase64 = [[NSData dataWithContentsOfURL:receiptURL] base64EncodedStringWithOptions:0]; + LOGI(@"Found appstore receipt %@", [receiptBase64 md5]); + return receiptBase64; +} + +- (void)validateReceipt: (SKPaymentTransaction*)transaction { + NSString *receiptBase64 = [self getReceipt]; + if (receiptBase64 == nil) { SKRequest* req = [[SKReceiptRefreshRequest alloc] init]; LOGI(@"Receipt not found yet, trying to retrieve it..."); req.delegate = self; [req start]; return; } - - receiptBase64 = [[NSData dataWithContentsOfURL:receiptURL] base64EncodedStringWithOptions:0]; - LOGI(@"Found appstore receipt %@", [receiptBase64 md5]); - //only check the receipt if it has changed if (latestReceiptMD5 == nil || ![latestReceiptMD5 isEqualToString:[receiptBase64 md5]]) { + // transaction is null when restoring user purchases at application start or if user clicks the "restore" button // We must validate the receipt on our server NSURL *URL = [NSURL URLWithString:[[LinphoneManager instance] lpConfigStringForKey:@"receipt_validation_url" forSection:@"in_app_purchase"]]; - XMLRPCRequest *request = [[XMLRPCRequest alloc] initWithURL: URL]; - - // transaction is null when restoring user purchases at application start or if user clicks the "restore" button - if (!transaction || [transaction.payment.productIdentifier isEqualToString:[[LinphoneManager instance] lpConfigStringForKey:@"paid_account_id" forSection:@"in_app_purchase"]]) { - //buying for the first time: need to create the account - //if ([transaction.transactionIdentifier isEqualToString:transaction.originalTransaction.transactionIdentifier]) { - if (self.accountCreationData.count == 3) { - [request setMethod: @"create_account_from_in_app_purchase" withParameters:[NSArray arrayWithObjects: - @"", - [_accountCreationData objectForKey:@"phoneNumber"], - receiptBase64, - @"", - @"apple", - [_accountCreationData objectForKey:@"email"], - nil]]; - self.accountCreationData = nil; - // otherwise simply renewing - } else { - if ([[self getPhoneNumber] length] > 0) { + //buying for the first time: need to create the account + //if ([transaction.transactionIdentifier isEqualToString:transaction.originalTransaction.transactionIdentifier]) { + if (self.accountCreationData.count == 3) { + [request setMethod: @"create_account_from_in_app_purchase" withParameters:[NSArray arrayWithObjects: + @"", + [_accountCreationData objectForKey:@"phoneNumber"], + receiptBase64, + @"", + @"apple", + [_accountCreationData objectForKey:@"email"], + nil]]; + self.accountCreationData = nil; + // otherwise simply renewing + } else { + if ([[self getPhoneNumber] length] > 0) { [request setMethod: @"get_expiration_date" withParameters:[NSArray arrayWithObjects: [self getPhoneNumber], receiptBase64, @"", @"apple", nil]]; - } else { - LOGW(@"No SIP URI configured, doing nothing"); - _available = true; - return; - } + } else { + LOGW(@"No SIP URI configured, doing nothing"); + _available = true; + return; } - } else { - LOGE(@"Hum, not handling product with ID %@", transaction.payment.productIdentifier); - _available = true; - return; } - latestReceiptMD5 = [[receiptBase64 md5] retain]; XMLRPCConnectionManager *manager = [XMLRPCConnectionManager sharedManager]; [manager spawnConnectionWithXMLRPCRequest: request delegate: self.xmlrpc]; - LOGI(@"XMLRPC query %@: %@", [request method], [request body]); + LOGI(@"XMLRPC query %@", [request method]); [request release]; } else { LOGW(@"Not checking receipt since it has already been done!"); @@ -311,7 +338,7 @@ NSDictionary* dict = @{@"product_id": transaction.payment.productIdentifier}; [self postNotificationforStatus:kIAPPurchaseCancelled withDict:dict]; } else { - NSString* errlast = [NSString stringWithFormat:@"Purchase of %@ failed: %@.",transaction.payment.productIdentifier,transaction.error.localizedDescription]; + NSString* errlast = [NSString stringWithFormat:@"Purchase failed: %@.",transaction.error.localizedDescription]; LOGE(@"SKPaymentTransactionStateFailed: %@", errlast); NSDictionary* dict = @{@"product_id": transaction.payment.productIdentifier, @"error_msg": errlast}; [self postNotificationforStatus:kIAPPurchaseFailed withDict:dict]; @@ -324,7 +351,7 @@ - (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions { for(SKPaymentTransaction * transaction in transactions) { - LOGI(@"%@ was removed from the payment queue.", transaction.payment.productIdentifier); + LOGD(@"%@ was removed from the payment queue.", transaction.payment.productIdentifier); } } @@ -350,6 +377,10 @@ _available = true; + if ([[request method] isEqualToString:@"activate_account"]) { + _accountActivationInProgress = NO; + } + LOGI(@"XMLRPC response %@: %@", [request method], [response body]); NSString* productID = [[LinphoneManager instance] lpConfigStringForKey:@"paid_account_id" forSection:@"in_app_purchase"]; @@ -361,22 +392,26 @@ // response object can either be expiration date (long long number or an error string) double timeinterval = [[response object] doubleValue]; if (timeinterval != 0.0f) { - NSDate *expirationDate = [NSDate dateWithTimeIntervalSince1970:timeinterval/1000]; - NSDate *now = [[NSDate alloc] init]; - NSDictionary* dict = @{@"product_id": productID, @"expires_date": expirationDate}; - if ([expirationDate earlierDate:now] == expirationDate) { + self.expirationDate = [NSDate dateWithTimeIntervalSince1970:timeinterval/1000]; + NSDate *now = [[[NSDate alloc] init] autorelease]; + NSDictionary* dict = @{@"product_id": productID, @"expires_date": self.expirationDate}; + if ([self.expirationDate earlierDate:now] == self.expirationDate) { LOGW(@"Account has expired"); [self postNotificationforStatus:kIAPPurchaseExpired withDict:dict]; } else { + LOGI(@"Account valid until %@", self.expirationDate); [_productsIDPurchased addObject:productID]; [self postNotificationforStatus:kIAPPurchaseSucceeded withDict:dict]; } } else { + self.expirationDate = nil; NSString *error = [response object]; LOGE(@"Failed with error %@", error); NSString *errorMsg; if ([error isEqualToString:@"ERROR_ACCOUNT_ALREADY_EXISTS"]) { - errorMsg=NSLocalizedString(@"You have already registered an account.", nil); + errorMsg=NSLocalizedString(@"This account is already registered.", nil); + } else if ([error isEqualToString:@"ERROR_UID_ALREADY_IN_USE"]) { + errorMsg=NSLocalizedString(@"You already own an account.", nil); } else if ([error isEqualToString:@"ERROR_ACCOUNT_DOESNT_EXIST"]) { errorMsg=NSLocalizedString(@"You have already purchased an account but it does not exist anymore.", nil); } else if ([error isEqualToString:@"ERROR_PURCHASE_CANCELLED"]) { @@ -415,6 +450,10 @@ _available = true; + if ([[request method] isEqualToString:@"activate_account"]) { + _accountActivationInProgress = NO; + } + LOGE(@"Communication issue (%@)", [error localizedDescription]); NSString *errorString = [NSString stringWithFormat:NSLocalizedString(@"Communication issue (%@)", nil), [error localizedDescription]]; UIAlertView* errorView = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Communication issue", nil) @@ -434,7 +473,7 @@ [[NSNotificationCenter defaultCenter] postNotificationName:status object:self userInfo:nil]; LOGE(@"Not supported, triggering %@", status); } -- (BOOL)purchaseAccount:(NSString *)phoneNumber withPassword:(NSString *)password andEmail:(NSString*)email { [self postNotificationforStatus:kIAPPurchaseFailed]; return false; } +- (BOOL)purchaseAccount:(NSString *)phoneNumber withPassword:(NSString *)password andEmail:(NSString *)email monthly:(BOOL)monthly { [self postNotificationforStatus:kIAPPurchaseFailed]; return false; } - (BOOL)restore { [self postNotificationforStatus:kIAPRestoreFailed]; return false; } - (BOOL)retrievePurchases { [self postNotificationforStatus:kIAPRestoreFailed]; return false; } - (BOOL)purchaseWitID:(NSString *)productID { [self postNotificationforStatus:kIAPPurchaseFailed]; return FALSE; } @@ -444,6 +483,7 @@ - (void)XMLRPCRequest:(XMLRPCRequest *)request didReceiveResponse:(XMLRPCResponse *)response { } - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { } - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { } +- (BOOL)activateAccount:(NSString *)phoneNumber { return FALSE; } #endif @end @@ -470,6 +510,6 @@ } - (void)request:(XMLRPCRequest *)request didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { - + } @end diff --git a/Classes/WizardViewController.m b/Classes/WizardViewController.m index 1d27f276b..874aa2094 100644 --- a/Classes/WizardViewController.m +++ b/Classes/WizardViewController.m @@ -933,7 +933,7 @@ static UICompositeViewDescription *compositeDescription = nil; NSString *identity = [self identityFromUsername:username]; [self checkUserExist:identity]; } else { - [iapm purchaseAccount:username withPassword:password andEmail:email]; + [iapm purchaseAccount:username withPassword:password andEmail:email monthly:FALSE]; // inAppPurchaseNotification will take care of bringing us to the next view now } }