/*
|
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
*
|
* This file is part of linphone-iphone
|
*
|
* This program is free software: you can redistribute it and/or modify
|
* it under the terms of the GNU General Public License as published by
|
* the Free Software Foundation, either version 3 of the License, or
|
* (at your option) any later version.
|
*
|
* This program is distributed in the hope that it will be useful,
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* GNU General Public License for more details.
|
*
|
* You should have received a copy of the GNU General Public License
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
*/
|
|
#import "InAppProductsManager.h"
|
#import "ShopView.h"
|
|
// In app purchase are not supported by the Simulator
|
#import "XMLRPCHelper.h"
|
#import "LinphoneManager.h"
|
#import "PhoneMainView.h"
|
#import "StoreKit/StoreKit.h"
|
|
@interface InAppProductsManager ()
|
@property(strong, nonatomic) NSDate *expirationDate;
|
@property(strong, nonatomic) NSDictionary *accountCreationData;
|
|
@end
|
|
|
|
@implementation InAppProductsManager
|
|
@synthesize checkPeriod;
|
@synthesize warnBeforeExpiryPeriod;
|
@synthesize notificationCategory;
|
|
// LINPHONE_CAPABILITY_INAPP_PURCHASE must be defined in Linphone Build Settings
|
#if 1
|
|
- (instancetype)init {
|
if ((self = [super init]) != nil) {
|
_enabled = (([SKPaymentQueue canMakePayments]) &&
|
([LinphoneManager.instance lpConfigBoolForKey:@"enabled" inSection:@"in_app_purchase"]));
|
_initialized = false;
|
_available = false;
|
_accountActivationInProgress = false;
|
checkPeriod = [LinphoneManager.instance lpConfigIntForKey:@"expiry_check_period" inSection:@"in_app_purchase"];
|
warnBeforeExpiryPeriod = [LinphoneManager.instance lpConfigIntForKey:@"warn_before_expiry_period" inSection:@"in_app_purchase"];
|
lastCheck = 0;
|
|
[XMLRPCHelper.self initArray];
|
//========// for test only
|
// int testExpiry = [LinphoneManager.instance lpConfigIntForKey:@"expiry_time_test"
|
// inSection:@"in_app_purchase"];
|
// if (testExpiry > 0){
|
// expiryTime = time(NULL) + testExpiry;
|
//}else expiryTime = 0;
|
//========//
|
if (_enabled) {
|
// self.xmlrpc = [[InAppProductsXMLRPCDelegate alloc] init];
|
_status = kIAPNotReady;
|
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
|
[self loadProducts];
|
[self checkAccountExpirationDate];
|
[self checkAccountTrial];
|
[self checkAccountExpired];
|
}
|
//[self check];
|
}
|
return self;
|
}
|
|
#pragma mark Public API
|
|
- (BOOL)isPurchasedWithID:(NSString *)productID {
|
if (!_enabled)
|
return FALSE;
|
|
for (NSString *prod in _productsIDPurchased) {
|
NSDate *now = [[NSDate alloc] init];
|
// 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;
|
}
|
}
|
return false;
|
}
|
|
- (BOOL)purchaseWithID:(NSString *)productID {
|
if (!_enabled || !_initialized || !_available) {
|
NSDictionary *dict = @{
|
@"product_id" : productID,
|
@"error_msg" : NSLocalizedString(@"Cannot purchase anything yet, please try again later.", nil)
|
};
|
[self postNotificationforStatus:kIAPPurchaseFailed withDict:dict];
|
return FALSE;
|
}
|
SKProduct *prod = [self productIDAvailable:productID];
|
if (prod) {
|
// Display waitview on click
|
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
|
UIView *topView = window.rootViewController.view;
|
UIView *waitview = (UIView *)[topView viewWithTag:288];
|
[waitview setHidden:FALSE];
|
|
NSDictionary *dict = @{ @"product_id" : productID };
|
[self postNotificationforStatus:kIAPPurchaseTrying withDict:dict];
|
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:prod];
|
_available = false;
|
[[SKPaymentQueue defaultQueue] addPayment:payment];
|
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
|
return TRUE;
|
} else {
|
NSDictionary *dict = @{ @"product_id" : productID, @"error_msg" : @"Product not available" };
|
[self postNotificationforStatus:kIAPPurchaseFailed withDict:dict];
|
return FALSE;
|
}
|
}
|
|
- (BOOL)restore {
|
if (!_enabled || !_initialized || !_available) {
|
NSDictionary *dict = @{ @"error_msg" : NSLocalizedString(@"In apps not ready yet", nil) };
|
[self postNotificationforStatus:kIAPRestoreFailed withDict:dict];
|
return FALSE;
|
}
|
LOGI(@"Restoring user purchases...");
|
// force new query of our server
|
latestReceiptMD5 = nil;
|
_available = false;
|
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
|
return TRUE;
|
}
|
|
- (BOOL)retrievePurchases {
|
if (!_enabled | !_initialized | !_available) {
|
NSDictionary *dict = @{ @"error_msg" : NSLocalizedString(@"In apps not ready yet", nil) };
|
[self postNotificationforStatus:kIAPRestoreFailed withDict:dict];
|
return FALSE;
|
} else if ([[self getPhoneNumber] length] == 0) {
|
LOGW(@"Not retrieving purchase since not account configured yet");
|
return FALSE;
|
} else {
|
_available = false;
|
[self validateReceipt:nil];
|
return TRUE;
|
}
|
}
|
|
#pragma mark ProductListLoading
|
|
- (void)loadProducts {
|
NSArray *list = [[[LinphoneManager.instance lpConfigStringForKey:@"products_list" inSection:@"in_app_purchase"]
|
stringByReplacingOccurrencesOfString:@" "
|
withString:@""] componentsSeparatedByString:@","];
|
|
_productsIDPurchased = [[NSMutableArray alloc] initWithCapacity:0];
|
|
SKProductsRequest *productsRequest =
|
[[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:list]];
|
productsRequest.delegate = self;
|
[productsRequest start];
|
}
|
|
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
|
_productsAvailable = [NSMutableArray arrayWithArray:response.products];
|
|
LOGI(@"Found %lu products available", (unsigned long)_productsAvailable.count);
|
_initialized = true;
|
if (response.invalidProductIdentifiers.count > 0) {
|
for (NSString *invalidIdentifier in response.invalidProductIdentifiers) {
|
LOGW(@"Found product Identifier with invalid ID '%@'", invalidIdentifier);
|
}
|
} else {
|
_available = true;
|
[self postNotificationforStatus:kIAPReady withDict:nil];
|
}
|
}
|
|
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
|
LOGE(@"Impossible to retrieve list of products: %@", [error localizedDescription]);
|
// well, let's retry...
|
[self loadProducts];
|
}
|
|
#pragma mark Other
|
|
- (SKProduct *)productIDAvailable:(NSString *)productID {
|
if (!_enabled || !_initialized)
|
return nil;
|
for (SKProduct *product in _productsAvailable) {
|
if ([product.productIdentifier compare:productID options:NSLiteralSearch] == NSOrderedSame) {
|
return product;
|
}
|
}
|
return nil;
|
}
|
|
- (void)requestDidFinish:(SKRequest *)request {
|
if ([request isKindOfClass:[SKReceiptRefreshRequest class]]) {
|
NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
|
if ([[NSFileManager defaultManager] fileExistsAtPath:[receiptUrl path]]) {
|
LOGI(@"App Receipt exists");
|
[self validateReceipt:nil];
|
} else {
|
// This can happen if the user cancels the login screen for the store.
|
// If we get here it means there is no receipt and an attempt to get it failed because the user cancelled
|
// the login.
|
LOGF(@"Receipt request done but there is no receipt");
|
}
|
}
|
}
|
|
#pragma mark Receipt management
|
|
- (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]);
|
[self saveReceiptTemp:receiptBase64];
|
return receiptBase64;
|
}
|
|
/**
|
Save Receipt temporarily until xmlrpc server request completed and confirmation sent
|
**/
|
- (void)saveReceiptTemp:(NSString *)receipt {
|
LOGE(@"===>>> saveReceiptTemp : TmpReceipt");
|
[LinphoneManager.instance lpConfigSetString:receipt forKey:@"save_tmp_receipt" inSection:@"in_app_purchase"];
|
}
|
|
/**
|
reset Receipt to empty after xmlrpc request confirmation received
|
**/
|
- (void)removeTmpReceipt:(NSString *)receipt {
|
LOGE(@"===>>> removeReceiptTemp : TmpReceipt");
|
if ([LinphoneManager.instance lpConfigStringForKey:@"save_tmp_receipt" inSection:@"in_app_purchase"])
|
[LinphoneManager.instance lpConfigSetString:@"0" forKey:@"save_tmp_receipt" inSection:@"in_app_purchase"];
|
}
|
|
/**
|
get temp Receipt to retry xmlrpc request
|
**/
|
- (NSString *)getTmpReceipt {
|
LOGE(@"===>>> getReceiptTemp : TmpReceipt");
|
return [LinphoneManager.instance lpConfigStringForKey:@"save_tmp_receipt" inSection:@"in_app_purchase"];
|
}
|
|
- (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;
|
}
|
// Hide waiting view
|
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
|
UIView *topView = window.rootViewController.view;
|
UIView *waitview = (UIView *)[topView viewWithTag:288];
|
[waitview setHidden:TRUE];
|
// only check the receipt if it has changed
|
if (latestReceiptMD5 == nil || ![latestReceiptMD5 isEqualToString:[receiptBase64 md5]]) {
|
[self updateAccountExpirationDate:receiptBase64];
|
latestReceiptMD5 = [receiptBase64 md5];
|
LOGI(@"XMLRPC query ");
|
} else {
|
LOGW(@"Not checking receipt since it has already been done!");
|
_available = true;
|
}
|
}
|
|
#pragma mark Getters
|
|
- (NSString *)getPhoneNumber {
|
NSString *phoneNumber = @"";
|
LinphoneAccount *defaultAccount = linphone_core_get_default_account(LC);
|
if (defaultAccount) {
|
const LinphoneAddress *identity = linphone_account_params_get_identity_address(linphone_account_get_params(defaultAccount));
|
if (identity) {
|
phoneNumber = [NSString stringWithUTF8String:linphone_address_get_username(identity)];
|
}
|
}
|
return phoneNumber;
|
}
|
|
- (NSString *)getPassword {
|
NSString *pass;
|
LinphoneAccount *defaultAccount = linphone_core_get_default_account(LC);
|
if (defaultAccount &&
|
strcmp([LinphoneManager.instance lpConfigStringForKey:@"domain_name"
|
inSection:@"app"
|
withDefault:@"sip.linphone.org"]
|
.UTF8String,
|
linphone_account_params_get_domain(linphone_account_get_params(defaultAccount))) == 0) {
|
const LinphoneAuthInfo *info = linphone_account_find_auth_info(defaultAccount);
|
const char *tmpPass;
|
if (linphone_auth_info_get_passwd(info)) {
|
tmpPass = linphone_auth_info_get_passwd(info);
|
} else {
|
tmpPass = linphone_auth_info_get_ha1(info);
|
}
|
pass = [NSString stringWithFormat:@"%s", tmpPass];
|
}
|
return pass;
|
}
|
|
#pragma mark Payment management
|
|
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
|
for (SKPaymentTransaction *transaction in transactions) {
|
switch (transaction.transactionState) {
|
case SKPaymentTransactionStatePurchasing:
|
break;
|
case SKPaymentTransactionStatePurchased:
|
case SKPaymentTransactionStateRestored: {
|
if (!_initialized) {
|
LOGW(@"Pending transactions before end of initialization, not verifying receipt");
|
} else {
|
[self validateReceipt:transaction];
|
}
|
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
|
break;
|
}
|
case SKPaymentTransactionStateDeferred:
|
LOGI(@"Waiting for parent approval...");
|
// could do some UI stuff
|
break;
|
case SKPaymentTransactionStateFailed: {
|
_available = true;
|
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
|
if (transaction.error.code == SKErrorPaymentCancelled) {
|
LOGI(@"SKPaymentTransactionStateFailed: cancelled");
|
NSDictionary *dict = @{ @"product_id" : transaction.payment.productIdentifier };
|
[self postNotificationforStatus:kIAPPurchaseCancelled withDict:dict];
|
} else {
|
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];
|
}
|
break;
|
}
|
}
|
}
|
}
|
|
- (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions {
|
for (SKPaymentTransaction *transaction in transactions) {
|
LOGD(@"%@ was removed from the payment queue.", transaction.payment.productIdentifier);
|
}
|
}
|
|
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error {
|
if (error.code != SKErrorPaymentCancelled) {
|
NSDictionary *dict = @{ @"error_msg" : [error localizedDescription] };
|
[self postNotificationforStatus:kIAPRestoreFailed withDict:dict];
|
}
|
}
|
|
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {
|
LOGI(@"All restorable transactions have been processed by the payment queue.");
|
}
|
|
- (void)postNotificationforStatus:(IAPPurchaseNotificationStatus)status withDict:(NSDictionary *)dict {
|
_status = status;
|
LOGI(@"Triggering notification for status %@", status);
|
[NSNotificationCenter.defaultCenter postNotificationName:status object:self userInfo:dict];
|
if ([status isEqual:kIAPPurchaseFailed] || [status isEqual:kIAPPurchaseCancelled]) {
|
// Hide waiting view
|
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
|
UIView *topView = window.rootViewController.view;
|
UIView *waitview = (UIView *)[topView viewWithTag:288];
|
[waitview setHidden:TRUE];
|
}
|
}
|
|
#pragma mark expiration notif
|
|
- (void) presentNotification:(int64_t) remaining{
|
if (notificationCategory == nil) return;
|
int days = (int)remaining / (3600 * 24);
|
NSString * expireText;
|
if (remaining >= 0){
|
expireText = [NSString stringWithFormat:NSLocalizedString(@"Your account will expire in %i days.", nil), days];
|
}else{
|
expireText = [NSString stringWithFormat:NSLocalizedString(@"Your account has expired.", nil), days];
|
}
|
|
if ([UIApplication sharedApplication].applicationState != UIApplicationStateActive) {
|
UILocalNotification *notification = [[UILocalNotification alloc] init];
|
if (notification) {
|
|
notification.category = notificationCategory;
|
notification.repeatInterval = 0;
|
notification.applicationIconBadgeNumber = 1;
|
notification.alertBody = expireText;
|
|
[[UIApplication sharedApplication] presentLocalNotificationNow:notification];
|
}
|
|
}else{
|
UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Account expiring", nil)
|
message:expireText
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
UIAlertAction* buyAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Buy", nil)
|
style:UIAlertActionStyleDefault
|
handler:^(UIAlertAction * action) {
|
[PhoneMainView.instance changeCurrentView:ShopView.compositeViewDescription];
|
}];
|
|
UIAlertAction *laterAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Later", nil)
|
style:UIAlertActionStyleCancel
|
handler:^(UIAlertAction *action){
|
// [alert dismissViewControllerAnimated:FALSE];
|
}];
|
|
[alert addAction:buyAction];
|
[alert addAction:laterAction];
|
[PhoneMainView.instance presentViewController:alert animated:YES completion:nil];
|
}
|
}
|
|
- (void) check{
|
if (!_available) return;
|
if (expiryTime == 0 || checkPeriod == 0) return;
|
|
time_t now = time(NULL);
|
|
if (now < lastCheck + checkPeriod) return;
|
if (now >= expiryTime - warnBeforeExpiryPeriod){
|
lastCheck = now;
|
int64_t remaining = (int64_t)expiryTime - (int64_t)now;
|
[self presentNotification: remaining];
|
}
|
|
if (![[self getTmpReceipt] isEqualToString:@""]) {
|
LOGE(@"===>>> Check : getTmpReceipt != ''");
|
[self updateAccountExpirationDate:[self getReceipt]];
|
}
|
}
|
|
#pragma mark Intermediate XMLRPC call method
|
|
// Intermediate method to check XMLRPC account expiration date
|
- (BOOL)updateAccountExpirationDate:(NSString *)receiptBase64 {
|
return [self
|
callXmlrpcRequestWithParams:@"update_expiration_date"
|
onSuccess:^(NSString *response) {
|
if (response) {
|
// NSString *productID = [LinphoneManager.instance
|
// lpConfigStringForKey:@"paid_account_id" inSection:@"in_app_purchase"];
|
LOGI(@"update_expiration_date callback - response: %@", response);
|
if ([response containsString:@"ERROR"]) {
|
LOGE(@"Failed with error %@", response);
|
NSString *errorMsg;
|
if ([response isEqualToString:@"ERROR_ACCOUNT_ALREADY_EXISTS"]) {
|
errorMsg = NSLocalizedString(@"This account is already connected.", nil);
|
} else if ([response isEqualToString:@"ERROR_UID_ALREADY_IN_USE"]) {
|
errorMsg = NSLocalizedString(@"You already own an account.", nil);
|
} else if ([response isEqualToString:@"ERROR_ACCOUNT_DOESNT_EXIST"]) {
|
errorMsg = NSLocalizedString(@"You have already purchased an account "
|
@"but it does not exist anymore.",
|
nil);
|
} else if ([response isEqualToString:@"ERROR_PURCHASE_CANCELLED"]) {
|
errorMsg = NSLocalizedString(@"You cancelled your account.", nil);
|
} else {
|
errorMsg = [NSString
|
stringWithFormat:NSLocalizedString(@"Unknown error (%@).", nil), response];
|
}
|
// NSDictionary *dict = @{ @"product_id" : productID, @"error_msg" :
|
// errorMsg };
|
//[self postNotificationforStatus:kIAPPurchaseFailed withDict:dict];
|
|
} else // remove temporarily receipt
|
[self removeTmpReceipt:receiptBase64];
|
}
|
}
|
onError:NULL
|
extra:receiptBase64];
|
}
|
|
// Intermediate method to check XMLRPC account expiration date
|
- (BOOL)checkAccountExpirationDate {
|
return [self callXmlrpcRequestWithParams:@"get_account_expiration"
|
onSuccess:^(NSString *response) {
|
if (response) {
|
LOGI(@"get_account_expiration callback - response: %@", response);
|
if ([response containsString:@"ERROR_NO_EXPIRATION"]) {
|
expiryTime = 0;
|
}
|
}
|
}
|
onError:NULL
|
extra:NULL];
|
}
|
|
// Intermediate method to check XMLRPC account trial
|
- (BOOL)checkAccountTrial {
|
return [self callXmlrpcRequestWithParams:@"is_account_trial"
|
onSuccess:^(NSString *response) {
|
if (response) {
|
LOGI(@"is_account_trial callback - response: %@", response);
|
}
|
}
|
onError:NULL
|
extra:NULL];
|
}
|
|
// Intermediate method to check XMLRPC account expired
|
- (BOOL)checkAccountExpired {
|
return [self callXmlrpcRequestWithParams:@"is_account_expired"
|
onSuccess:^(NSString *response) {
|
if (response) {
|
LOGI(@"is_account_expired callback - response: %@", response);
|
}
|
}
|
onError:NULL
|
extra:NULL];
|
}
|
|
// Intermediate method to check check payload signature
|
- (BOOL)checkPayloadSignature {
|
return [self callXmlrpcRequestWithParams:@"check_payload_signature"
|
onSuccess:^(NSString *response) {
|
if (response) {
|
LOGI(@"check_payload_signature callback - response: %@", response);
|
}
|
}
|
onError:NULL
|
extra:NULL];
|
}
|
|
// Generic function to call sendXMLRPCRequestWithParams
|
- (BOOL)callXmlrpcRequestWithParams:(NSString *)method
|
onSuccess:(void (^)(NSString *))successBk
|
onError:(void (^)(NSString *req))errorBk
|
extra:(NSString *)extra {
|
if ([[self getPhoneNumber] length] > 0) {
|
NSString *phoneNumber = [self getPhoneNumber];
|
NSString *password = [self getPassword];
|
NSArray *args;
|
if (extra != NULL)
|
args = @[ phoneNumber, password, @"", extra ];
|
else
|
args = @[ phoneNumber, password, @"" ];
|
if (successBk && errorBk)
|
[XMLRPCHelper.self sendXMLRPCRequestWithParams:method withParams:args onSuccess:successBk onError:errorBk];
|
else if (successBk)
|
[XMLRPCHelper.self sendXMLRPCRequestWithParams:method withParams:args onSuccess:successBk];
|
else
|
[XMLRPCHelper.self sendXMLRPCRequestWithParams:method withParams:args];
|
return TRUE;
|
|
} else {
|
LOGW(@"No SIP URI configured, can't get account expiration date.");
|
return FALSE;
|
}
|
}
|
|
#pragma mark Other
|
#else
|
- (void)postNotificationforStatus:(IAPPurchaseNotificationStatus)status {
|
_status = status;
|
[NSNotificationCenter.defaultCenter postNotificationName:status object:self userInfo:nil];
|
LOGE(@"Not supported, triggering %@", status);
|
}
|
|
#endif
|
@end
|