diff --git a/.tx/config b/.tx/config index cf79ceeeb..1b9158716 100644 --- a/.tx/config +++ b/.tx/config @@ -4,7 +4,10 @@ minimum_perc = 1 [linphone-ios.localizablestrings] file_filter = Resources/.lproj/Localizable.strings -source_file = Resources/en.lproj/Localizable.strings +source_lang = en + +[linphone-ios.localizablestrings] +file_filter = Resources/InAppSettings.bundle/.lproj/Root.strings source_lang = en [linphone-ios.aboutviewcontrollerstrings] diff --git a/Classes/Base.lproj/WizardViews.xib b/Classes/Base.lproj/WizardViews.xib index f9e4cd040..4a7bb5885 100644 --- a/Classes/Base.lproj/WizardViews.xib +++ b/Classes/Base.lproj/WizardViews.xib @@ -148,7 +148,6 @@ - diff --git a/Classes/InAppProductsCell.m b/Classes/InAppProductsCell.m index 229e7751f..6062261d6 100644 --- a/Classes/InAppProductsCell.m +++ b/Classes/InAppProductsCell.m @@ -33,6 +33,7 @@ [_pdescription setText: [prod localizedDescription]]; [_pprice setText: formattedPrice]; [_ppurchased setOn: [[[LinphoneManager instance] iapManager] isPurchasedWithID:prod.productIdentifier]]; + _productID = prod.productIdentifier; } - (id)initWithIdentifier:(NSString*)identifier maximized:(bool)maximized { if ((self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]) != nil) { diff --git a/Classes/InAppProductsManager.h b/Classes/InAppProductsManager.h index 68f59e8ab..95197463c 100644 --- a/Classes/InAppProductsManager.h +++ b/Classes/InAppProductsManager.h @@ -20,8 +20,13 @@ #import #import +#import + extern NSString *const kLinphoneIAPurchaseNotification; +@interface InAppProductsXMLRPCDelegate : NSObject +@end + @interface InAppProductsManager : NSObject #define IAPAvailableSucceeded @"IAPAvailableSucceeded" @@ -35,6 +40,9 @@ extern NSString *const kLinphoneIAPurchaseNotification; typedef NSString* IAPPurchaseNotificationStatus; +// needed because request:didFailWithError method is already used by SKProductsRequestDelegate... +@property (nonatomic, retain) InAppProductsXMLRPCDelegate *xmlrpc; + @property (nonatomic, retain) IAPPurchaseNotificationStatus status; @property (nonatomic, copy) NSString *errlast; @@ -45,5 +53,8 @@ typedef NSString* IAPPurchaseNotificationStatus; - (BOOL)isPurchasedWithID:(NSString*)productId; - (void)purchaseWithID:(NSString*)productId; - (void)restore; +- (void)retrievePurchases; +- (void)XMLRPCRequest:(XMLRPCRequest *)request didReceiveResponse:(XMLRPCResponse *)response; +- (void)XMLRPCRequest:(XMLRPCRequest *)request didFailWithError:(NSError *)error; @end diff --git a/Classes/InAppProductsManager.m b/Classes/InAppProductsManager.m index b68f453e9..7a1af2131 100644 --- a/Classes/InAppProductsManager.m +++ b/Classes/InAppProductsManager.m @@ -18,27 +18,64 @@ */ #import "InAppProductsManager.h" + +#import +#import +#import +#import + #import "Utils.h" #import "LinphoneManager.h" NSString *const kLinphoneIAPurchaseNotification = @"LinphoneIAProductsNotification"; + +@implementation InAppProductsXMLRPCDelegate { + InAppProductsManager *iapm; +} + +#pragma mark - XMLRPCConnectionDelegate Functions + +- (void)request:(XMLRPCRequest *)request didReceiveResponse:(XMLRPCResponse *)response { + [[[LinphoneManager instance] iapManager] XMLRPCRequest:request didReceiveResponse:response]; +} + +- (void)request:(XMLRPCRequest *)request didFailWithError:(NSError *)error { + [[[LinphoneManager instance] iapManager] XMLRPCRequest:request didFailWithError:error]; +} + +- (BOOL)request:(XMLRPCRequest *)request canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace { + return FALSE; +} + +- (void)request:(XMLRPCRequest *)request didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { + +} + +- (void)request:(XMLRPCRequest *)request didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { + +} + +@end + @implementation InAppProductsManager { } - (instancetype)init { if ((self = [super init]) != nil) { + _xmlrpc = [[InAppProductsXMLRPCDelegate alloc] init]; [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; [self loadProducts]; } return self; } +#define INAPP_AVAIL() ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0) && ([SKPaymentQueue canMakePayments]) + - (void)loadProducts { - if (! [SKPaymentQueue canMakePayments]) { - return; - } - NSArray * list = [[[[LinphoneManager instance] lpConfigStringForKey:@"inapp_products_list"] stringByReplacingOccurrencesOfString:@" " withString:@""] componentsSeparatedByString:@","]; + if (!INAPP_AVAIL()) return; + + NSArray * list = [[[[LinphoneManager instance] lpConfigStringForKey:@"products_list" forSection:@"in_app_purchase"] stringByReplacingOccurrencesOfString:@" " withString:@""] componentsSeparatedByString:@","]; _productsIDPurchased = [[NSMutableArray alloc] initWithCapacity:0]; @@ -75,9 +112,10 @@ NSString *const kLinphoneIAPurchaseNotification = @"LinphoneIAProductsNotificati return false; } -- (void)purchaseWithID:(NSString *)productId { +- (void)purchaseWithID:(NSString *)productID { + LOGI(@"Trying to purchase %@", productID); for (SKProduct *product in _productsAvailable) { - if ([product.productIdentifier compare:productId options:NSLiteralSearch] == NSOrderedSame) { + if ([product.productIdentifier compare:productID options:NSLiteralSearch] == NSOrderedSame) { SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product]; [[SKPaymentQueue defaultQueue] addPayment:payment]; return; @@ -88,53 +126,49 @@ NSString *const kLinphoneIAPurchaseNotification = @"LinphoneIAProductsNotificati -(void)restore { LOGI(@"Restoring user purchases..."); - if (! [SKPaymentQueue canMakePayments]) { - LOGF(@"Not allowed to do in app purchase!!!"); - } [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; } +static SKRequest* req = nil; + +- (void)requestDidFinish:(SKRequest *)request { + if (req == request) { + LOGI(@"Got receipt"); + [self checkReceipt:nil]; + } +} + +- (void)request:(SKRequest *)skrequest didFailWithError:(NSError *)error { + LOGE(@"Did not get receipt: %@", error); + [self setStatus:IAPReceiptFailed]; +} + - (void)checkReceipt: (SKPaymentTransaction*)transaction { NSData *receiptData = nil; NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; // Test whether the receipt is present at the above URL if(![[NSFileManager defaultManager] fileExistsAtPath:[receiptURL path]]) { - receiptData = [NSData dataWithContentsOfURL:receiptURL]; + receiptData = [NSData dataWithContentsOfURL:receiptURL]; + LOGI(@"Found appstore receipt containing: %@", receiptData); + } else if (req == nil) { + // We are probably in sandbox environment, trying to retrieve it... + req = [[SKReceiptRefreshRequest alloc] init]; + LOGI(@"Receipt not found yet, trying to retrieve it..."); + req.delegate = self; + [req start]; } else { - LOGI(@"Could not find any receipt in application, using the transaction one!"); - receiptData = transaction.transactionReceipt; + LOGF(@"No receipt found"); } // We must validate the receipt on our server - NSURL *storeURL = [NSURL URLWithString:[[LinphoneManager instance] lpConfigStringForKey:@"inapp_receipt_validation_url"]]; - NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL]; - [storeRequest setHTTPMethod:@"POST"]; - [storeRequest setHTTPBody:receiptData]; + NSURL *URL = [NSURL URLWithString:[[LinphoneManager instance] lpConfigStringForKey:@"receipt_validation_url" forSection:@"in_app_purchase"]]; - NSOperationQueue *queue = [[NSOperationQueue alloc] init]; - [NSURLConnection sendAsynchronousRequest:storeRequest - queue:queue - completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { - if (data == nil) { - LOGE(@"Server replied nothing, aborting now."); - } else if (connectionError) { - LOGE(@"Could not verify the receipt, aborting now: (%d) %@.", connectionError.code, connectionError); - } else { - NSError *jsonError; - NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; - if (jsonError == nil) { - // Hourray, we can finally unlock purchase stuff in app! - LOGI(@"In apps purchased are %@", jsonResponse); - LOGE(@"To do: [_productsIDPurchased addObject:transaction.payment.productIdentifier];"); - [self postNotificationforStatus:IAPReceiptSucceeded]; - return; - } else { - LOGE(@"Impossible to parse receipt JSON response, aborting now because of %@: %@.", jsonError, [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]); - } - } - // Something failed when checking the receipt - [self postNotificationforStatus:IAPReceiptFailed]; - }]; + XMLRPCRequest *request = [[XMLRPCRequest alloc] initWithURL: URL]; + [request setMethod: @"in_app_receipt_validation" withParameters:[NSArray arrayWithObjects:@"ios", receiptData, nil]]; + XMLRPCConnectionManager *manager = [XMLRPCConnectionManager sharedManager]; + [manager spawnConnectionWithXMLRPCRequest: request delegate: self.xmlrpc]; + + [request release]; } - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { @@ -147,23 +181,12 @@ NSString *const kLinphoneIAPurchaseNotification = @"LinphoneIAProductsNotificati [self checkReceipt: transaction]; [self completeTransaction:transaction forStatus:IAPPurchaseSucceeded]; break; - -// case SKPaymentTransactionStatePurchasing: -// break; -// case SKPaymentTransactionStateDeferred: -// [self transactionDeferred:transaction]; -// break; -// case SKPaymentTransactionStatePurchased: -// [self completeTransaction:transaction]; -// break; -// case SKPaymentTransactionStateRestored: -// [self restoreTransaction:transaction]; -// break; -// case SKPaymentTransactionStateFailed: -// [self failedTransaction:transaction]; -// break; - default: + case SKPaymentTransactionStateDeferred: + //waiting for parent approval + break; + case SKPaymentTransactionStateFailed: _errlast = [NSString stringWithFormat:@"Purchase of %@ failed.",transaction.payment.productIdentifier]; + LOGE(@"%@", _errlast); [self completeTransaction:transaction forStatus:IAPPurchaseFailed]; break; } @@ -204,4 +227,57 @@ NSString *const kLinphoneIAPurchaseNotification = @"LinphoneIAProductsNotificati [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; } +- (void)retrievePurchases { + [self checkReceipt:nil]; +} + +- (void)XMLRPCRequest:(XMLRPCRequest *)request didReceiveResponse:(XMLRPCResponse *)response { + LOGI(@"XMLRPC %@: %@", [request method], [response body]); + // [waitView setHidden:true]; + if ([response isFault]) { + LOGE(@"Communication issue (%@)", [response faultString]); + // NSString *errorString = [NSString stringWithFormat:NSLocalizedString(@"Communication issue (%@)", nil), [response faultString]]; + // UIAlertView* errorView = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Communication issue",nil) + // message:errorString + // delegate:nil + // cancelButtonTitle:NSLocalizedString(@"Continue",nil) + // otherButtonTitles:nil,nil]; + // [errorView show]; + // [errorView release]; + } else if([response object] != nil) { + //Don't handle if not object: HTTP/Communication Error + if([[request method] isEqualToString:@"check_account"]) { + if([response object] == [NSNumber numberWithInt:0]) { + // UIAlertView* errorView = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Check issue",nil) + // message:NSLocalizedString(@"Username already exists", nil) + // delegate:nil + // cancelButtonTitle:NSLocalizedString(@"Continue",nil) + // otherButtonTitles:nil,nil]; + // [errorView show]; + // [errorView release]; + [self.productsIDPurchased addObject:@"test.one_time"]; + [self postNotificationforStatus:IAPReceiptSucceeded]; + return; + } + } + } + [self postNotificationforStatus:IAPReceiptFailed]; +} + +- (void)XMLRPCRequest:(XMLRPCRequest *)request didFailWithError:(NSError *)error { + LOGE(@"Communication issue (%@)", [error localizedDescription]); + [self.productsIDPurchased addObject:@"test.one_time"]; + // NSString *errorString = [NSString stringWithFormat:NSLocalizedString(@"Communication issue (%@)", nil), [error localizedDescription]]; + // UIAlertView* errorView = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Communication issue", nil) + // message:errorString + // delegate:nil + // cancelButtonTitle:NSLocalizedString(@"Continue", nil) + // otherButtonTitles:nil,nil]; + // [errorView show]; + // [errorView release]; + // [waitView setHidden:true]; + + [self postNotificationforStatus:IAPReceiptFailed]; +} @end + diff --git a/Classes/InAppProductsTableViewController.m b/Classes/InAppProductsTableViewController.m index a45fdbcd8..1e046d47c 100644 --- a/Classes/InAppProductsTableViewController.m +++ b/Classes/InAppProductsTableViewController.m @@ -92,7 +92,6 @@ [alert release]; } else { //try to purchase item, and if successfull change the switch - LOGI(@"Trying to purchase %@", cell.productID); [[[LinphoneManager instance] iapManager] purchaseWithID: cell.productID]; } } diff --git a/Classes/LinphoneManager.m b/Classes/LinphoneManager.m index ff2f18203..af6c4b5c1 100644 --- a/Classes/LinphoneManager.m +++ b/Classes/LinphoneManager.m @@ -1370,6 +1370,8 @@ static LinphoneCoreVTable linphonec_vtable = { } linphone_core_enable_video(theLinphoneCore, FALSE, FALSE); } + // Retrieve InApp purchases + [_iapManager retrievePurchases]; LOGW(@"Linphone [%s] started on [%s]", linphone_core_get_version(), [[UIDevice currentDevice].model cStringUsingEncoding:[NSString defaultCStringEncoding]]); @@ -2257,5 +2259,7 @@ static void audioRouteChangeListenerCallback ( } } +#pragma InApp Purchase + @end diff --git a/Classes/WizardViewController.m b/Classes/WizardViewController.m index 8e270c372..36a4b4aa1 100644 --- a/Classes/WizardViewController.m +++ b/Classes/WizardViewController.m @@ -733,20 +733,23 @@ static UICompositeViewDescription *compositeDescription = nil; - (void)inAppPurchaseNotification: (NSNotification*)notification { InAppProductsManager *iapm = [[LinphoneManager instance] iapManager]; - if ([iapm isPurchasedWithID:[[LinphoneManager instance] lpConfigStringForKey:@"inapp_paid_account_id" forSection:@"wizard"]]) { - nextView = createAccountView; - [self loadWizardConfig:@"wizard_linphone_create.rc"]; + if ([iapm isPurchasedWithID:[[LinphoneManager instance] lpConfigStringForKey:@"paid_account_id" forSection:@"in_app_purchase"]]) { + [self onPurchaseAccountClick:self]; } } - (IBAction)onPurchaseAccountClick:(id)sender { InAppProductsManager *iapm = [[LinphoneManager instance] iapManager]; //if has already purchased, continue - if (false) { - nextView = createAccountView; - [self loadWizardConfig:@"wizard_linphone_create.rc"]; + if ([iapm isPurchasedWithID:[[LinphoneManager instance] lpConfigStringForKey:@"paid_account_id" forSection:@"in_app_purchase"]]) { + NSString *username = [WizardViewController findTextField:ViewElement_Username view:contentView].text; + NSString *password = [WizardViewController findTextField:ViewElement_Password view:contentView].text; + NSString *domain = [WizardViewController findTextField:ViewElement_Domain view:contentView].text; + NSString *transport = [self.transportChooser titleForSegmentAtIndex:self.transportChooser.selectedSegmentIndex]; + + [self verificationSignInWithUsername:username password:password domain:domain withTransport:transport]; } else { - [iapm purchaseWithID: [[LinphoneManager instance] lpConfigStringForKey:@"inapp_paid_account_id" forSection:@"wizard"]]; + [iapm purchaseWithID: [[LinphoneManager instance] lpConfigStringForKey:@"paid_account_id" forSection:@"in_app_purchase"]]; } } diff --git a/Resources/linphonerc-factory b/Resources/linphonerc-factory index 7c86ff681..9d20c530c 100644 --- a/Resources/linphonerc-factory +++ b/Resources/linphonerc-factory @@ -47,7 +47,6 @@ transport=tls sharing_server=https://www.linphone.org:444/upload.php ice=1 stun=stun.linphone.org -inapp_paid_account_id = test.auto_renew_7days [video] display_filter_auto_rotate=0 @@ -55,5 +54,9 @@ display_filter_auto_rotate=0 [app] #contact_display_username_only=1 #contact_filter_on_default_domain=1 -inapp_receipt_validation_url = https://linphone.org/inapp/veriyReceipt -inapp_products_list = test.auto_renew_7days, test.non_renew, test.one_time, test.auto_renew_1month_withfree + +[in_app_purchase] +enabled = 1 +paid_account_id = test.auto_renew_7days +receipt_validation_url = https://www.linphone.org/inapp.php +products_list = test.auto_renew_7days, test.non_renew, test.one_time, test.auto_renew_1month_withfree diff --git a/Resources/linphonerc-factory~ipad b/Resources/linphonerc-factory~ipad index afc14fc79..aa3a99255 100644 --- a/Resources/linphonerc-factory~ipad +++ b/Resources/linphonerc-factory~ipad @@ -47,11 +47,12 @@ transport=tls sharing_server=https://www.linphone.org:444/upload.php ice=1 stun=stun.linphone.org -inapp_paid_account_id = test.auto_renew_7days [video] display_filter_auto_rotate=0 -[app] -inapp_receipt_validation_url = https://linphone.org/inapp/veriyReceipt -inapp_products_list = test.auto_renew_7days, test.non_renew, test.one_time, test.auto_renew_1month_withfree \ No newline at end of file +[in_app_purchase] +enabled = 1 +paid_account_id = test.auto_renew_7days +receipt_validation_url = https://www.linphone.org/inapp.php +products_list = test.auto_renew_7days, test.non_renew, test.one_time, test.auto_renew_1month_withfree \ No newline at end of file diff --git a/linphone-Info.plist b/linphone-Info.plist index 964a1563d..57479d407 100644 --- a/linphone-Info.plist +++ b/linphone-Info.plist @@ -53,7 +53,7 @@ CFBundleVersion - 2 + 4 LSRequiresIPhoneOS UIApplicationExitsOnSuspend diff --git a/linphone.xcodeproj/project.pbxproj b/linphone.xcodeproj/project.pbxproj index 12f7f743a..c340b59ba 100755 --- a/linphone.xcodeproj/project.pbxproj +++ b/linphone.xcodeproj/project.pbxproj @@ -936,7 +936,6 @@ 1D3623250D0F684500981E51 /* LinphoneAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LinphoneAppDelegate.m; sourceTree = ""; }; 1D6058910D05DD3D006BFB54 /* linphone.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = linphone.app; sourceTree = BUILT_PRODUCTS_DIR; }; 1DF5F4DF0D08C38300B7A737 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; - 1FE76362DA6217E7341ED1DF /* libPods-KifTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-KifTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 220FAD2910765B400068D98F /* libgsm.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgsm.a; path = "liblinphone-sdk/apple-darwin/lib/libgsm.a"; sourceTree = ""; }; 220FAD2C10765B400068D98F /* libortp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libortp.a; path = "liblinphone-sdk/apple-darwin/lib/libortp.a"; sourceTree = ""; }; 220FAD2F10765B400068D98F /* libspeex.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libspeex.a; path = "liblinphone-sdk/apple-darwin/lib/libspeex.a"; sourceTree = ""; }; @@ -2310,7 +2309,6 @@ 22509041196BD902007863F6 /* libopenh264.a */, 22AF73C11754C0D000BE8398 /* libopus.a */, 220FAD2C10765B400068D98F /* libortp.a */, - 1FE76362DA6217E7341ED1DF /* libPods-KifTests.a */, 57B0E35F173C010400A476B8 /* libpolarssl.a */, F0BB8C34193624C800974404 /* libresolv.9.dylib */, 22D1B68012A3E0BE001AE361 /* libresolv.dylib */, @@ -5336,6 +5334,7 @@ HEADER_SEARCH_PATHS = ( "$(inherited)", "$(SRCROOT)/liblinphone-sdk/apple-darwin/include", + "$(SRCROOT)/Classes/Utils/XMLRPC/", ); INFOPLIST_FILE = KifTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; @@ -5380,6 +5379,7 @@ HEADER_SEARCH_PATHS = ( "$(inherited)", "$(SRCROOT)/liblinphone-sdk/apple-darwin/include", + "$(SRCROOT)/Classes/Utils/XMLRPC/", ); INFOPLIST_FILE = KifTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; @@ -5424,6 +5424,7 @@ HEADER_SEARCH_PATHS = ( "$(inherited)", "$(SRCROOT)/liblinphone-sdk/apple-darwin/include", + "$(SRCROOT)/Classes/Utils/XMLRPC/", ); INFOPLIST_FILE = KifTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; @@ -5468,6 +5469,7 @@ HEADER_SEARCH_PATHS = ( "$(inherited)", "$(SRCROOT)/liblinphone-sdk/apple-darwin/include", + "$(SRCROOT)/Classes/Utils/XMLRPC/", ); INFOPLIST_FILE = KifTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";