InAppProductsManager: add some comments and add account activation feature

This commit is contained in:
Gautier Pelloux-Prayer 2015-05-29 10:33:12 +02:00
parent 728b84f7ee
commit 79a297d1a9
3 changed files with 109 additions and 57 deletions

View file

@ -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 <SKProductsRequestDelegate, SKPaymentTransactionObserver> {
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;

View file

@ -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

View file

@ -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
}
}