2017-04-05 20:56:22 +00:00
|
|
|
//==============================================================================
|
|
|
|
// This file is part of Master Password.
|
|
|
|
// Copyright (c) 2011-2017, Maarten Billemont.
|
2014-09-21 14:49:57 +00:00
|
|
|
//
|
2017-04-05 20:56:22 +00:00
|
|
|
// Master Password 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.
|
2014-09-21 14:49:57 +00:00
|
|
|
//
|
2017-04-05 20:56:22 +00:00
|
|
|
// Master Password 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.
|
2014-09-21 14:49:57 +00:00
|
|
|
//
|
2017-04-05 20:56:22 +00:00
|
|
|
// You can find a copy of the GNU General Public License in the
|
|
|
|
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
|
|
|
|
//==============================================================================
|
2014-09-21 14:49:57 +00:00
|
|
|
|
|
|
|
#import "MPAppDelegate_InApp.h"
|
2020-01-27 18:27:10 +00:00
|
|
|
#import <Countly/Countly.h>
|
2014-09-24 05:07:02 +00:00
|
|
|
|
|
|
|
@interface MPAppDelegate_Shared(InApp_Private)<SKProductsRequestDelegate, SKPaymentTransactionObserver>
|
|
|
|
@end
|
2014-09-21 14:49:57 +00:00
|
|
|
|
|
|
|
@implementation MPAppDelegate_Shared(InApp)
|
|
|
|
|
2017-04-30 03:03:50 +00:00
|
|
|
PearlAssociatedObjectProperty( NSDictionary*, Products, products );
|
|
|
|
|
2014-09-26 04:32:07 +00:00
|
|
|
PearlAssociatedObjectProperty( NSMutableArray*, ProductObservers, productObservers );
|
2014-09-21 14:49:57 +00:00
|
|
|
|
2017-04-30 21:48:03 +00:00
|
|
|
- (NSDictionary<NSString *, SKPaymentTransaction *> *)transactions {
|
|
|
|
|
|
|
|
NSMutableDictionary<NSString *, SKPaymentTransaction *> *transactions =
|
|
|
|
[NSMutableDictionary dictionaryWithCapacity:self.paymentQueue.transactions.count];
|
|
|
|
for (SKPaymentTransaction *transaction in self.paymentQueue.transactions)
|
|
|
|
transactions[transaction.payment.productIdentifier] = transaction;
|
|
|
|
|
|
|
|
return transactions;
|
|
|
|
}
|
|
|
|
|
2014-09-26 04:32:07 +00:00
|
|
|
- (void)registerProductsObserver:(id<MPInAppDelegate>)delegate {
|
|
|
|
|
|
|
|
if (!self.productObservers)
|
|
|
|
self.productObservers = [NSMutableArray array];
|
|
|
|
[self.productObservers addObject:delegate];
|
|
|
|
|
|
|
|
if (self.products)
|
2017-04-30 21:48:03 +00:00
|
|
|
[delegate updateWithProducts:self.products transactions:[self transactions]];
|
2014-09-26 04:32:07 +00:00
|
|
|
else
|
|
|
|
[self reloadProducts];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)removeProductsObserver:(id<MPInAppDelegate>)delegate {
|
|
|
|
|
|
|
|
[self.productObservers removeObject:delegate];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)reloadProducts {
|
2014-09-21 14:49:57 +00:00
|
|
|
|
2014-09-24 05:07:02 +00:00
|
|
|
SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:
|
2016-01-14 15:03:30 +00:00
|
|
|
[[NSSet alloc] initWithObjects:MPProductGenerateLogins, MPProductGenerateAnswers, MPProductTouchID, MPProductFuel, nil]];
|
2014-09-24 05:07:02 +00:00
|
|
|
productsRequest.delegate = self;
|
|
|
|
[productsRequest start];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (SKPaymentQueue *)paymentQueue {
|
|
|
|
|
2014-09-21 14:49:57 +00:00
|
|
|
static dispatch_once_t once = 0;
|
|
|
|
dispatch_once( &once, ^{
|
|
|
|
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
|
|
|
|
} );
|
|
|
|
|
2014-09-24 05:07:02 +00:00
|
|
|
return [SKPaymentQueue defaultQueue];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL)canMakePayments {
|
|
|
|
|
|
|
|
return [SKPaymentQueue canMakePayments];
|
2014-09-21 14:49:57 +00:00
|
|
|
}
|
|
|
|
|
2014-09-26 04:32:07 +00:00
|
|
|
- (BOOL)isFeatureUnlocked:(NSString *)productIdentifier {
|
|
|
|
|
|
|
|
if (![productIdentifier length])
|
|
|
|
// Missing a product.
|
|
|
|
return NO;
|
|
|
|
if ([productIdentifier isEqualToString:MPProductFuel])
|
|
|
|
// Consumable product.
|
|
|
|
return NO;
|
|
|
|
|
2017-04-19 00:30:06 +00:00
|
|
|
#if DEBUG
|
2016-05-16 04:50:17 +00:00
|
|
|
// All features are unlocked for beta / debug / mac versions.
|
2014-09-26 04:32:07 +00:00
|
|
|
return YES;
|
|
|
|
#else
|
|
|
|
// Check if product is purchased.
|
|
|
|
return [[NSUserDefaults standardUserDefaults] objectForKey:productIdentifier] != nil;
|
|
|
|
#endif
|
2014-09-21 14:49:57 +00:00
|
|
|
}
|
|
|
|
|
2014-09-24 05:07:02 +00:00
|
|
|
- (void)restoreCompletedTransactions {
|
|
|
|
|
|
|
|
[[self paymentQueue] restoreCompletedTransactions];
|
|
|
|
}
|
|
|
|
|
2014-09-26 04:32:07 +00:00
|
|
|
- (void)purchaseProductWithIdentifier:(NSString *)productIdentifier quantity:(NSInteger)quantity {
|
2014-09-24 05:07:02 +00:00
|
|
|
|
2014-09-29 02:15:55 +00:00
|
|
|
#if TARGET_OS_IPHONE
|
|
|
|
if (![[MPAppDelegate_Shared get] canMakePayments]) {
|
2020-01-14 18:59:32 +00:00
|
|
|
UIAlertController *controller = [UIAlertController alertControllerWithTitle:@"App Store Not Set Up" message:
|
2017-05-07 22:36:01 +00:00
|
|
|
@"Make sure your Apple ID is set under Settings -> iTunes & App Store, "
|
2020-01-14 18:59:32 +00:00
|
|
|
@"you have a payment method added to the account and purchases are"
|
|
|
|
@"not disabled under General -> Restrictions."
|
|
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
[controller addAction:[UIAlertAction actionWithTitle:@"Settings" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
|
|
|
|
[PearlLinks openSettingsStore];
|
|
|
|
}]];
|
|
|
|
[controller addAction:[UIAlertAction actionWithTitle:@"Try Anyway" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
|
|
|
|
[self performPurchaseProductWithIdentifier:productIdentifier quantity:quantity];
|
|
|
|
}]];
|
|
|
|
[controller addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
|
|
|
|
[self.navigationController presentViewController:controller animated:YES completion:nil];
|
2014-09-29 02:15:55 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2017-05-08 18:58:46 +00:00
|
|
|
[self performPurchaseProductWithIdentifier:productIdentifier quantity:quantity];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)performPurchaseProductWithIdentifier:(NSString *)productIdentifier quantity:(NSInteger)quantity {
|
|
|
|
|
2017-04-30 03:03:50 +00:00
|
|
|
for (SKProduct *product in [self.products allValues])
|
2014-09-24 05:07:02 +00:00
|
|
|
if ([product.productIdentifier isEqualToString:productIdentifier]) {
|
2014-09-26 04:32:07 +00:00
|
|
|
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
|
2017-04-30 03:03:50 +00:00
|
|
|
if (payment) {
|
|
|
|
payment.quantity = quantity;
|
|
|
|
[[self paymentQueue] addPayment:payment];
|
|
|
|
}
|
2014-09-24 05:07:02 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-09-21 14:49:57 +00:00
|
|
|
#pragma mark - SKProductsRequestDelegate
|
|
|
|
|
|
|
|
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
|
|
|
|
|
2017-04-30 03:03:50 +00:00
|
|
|
if ([response.invalidProductIdentifiers count])
|
|
|
|
inf( @"Invalid products: %@", response.invalidProductIdentifiers );
|
|
|
|
|
|
|
|
NSMutableDictionary *products = [NSMutableDictionary dictionaryWithCapacity:[response.products count]];
|
|
|
|
for (SKProduct *product in response.products)
|
|
|
|
products[product.productIdentifier] = product;
|
|
|
|
self.products = products;
|
2014-09-26 04:32:07 +00:00
|
|
|
|
2020-01-16 21:20:16 +00:00
|
|
|
PearlMainQueue( ^{
|
|
|
|
for (id<MPInAppDelegate> productObserver in self.productObservers)
|
|
|
|
[productObserver updateWithProducts:self.products transactions:[self transactions]];
|
|
|
|
} );
|
2014-09-21 14:49:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
|
|
|
|
|
2017-04-30 03:03:50 +00:00
|
|
|
MPError( error, @"StoreKit request (%@) failed.", request );
|
|
|
|
|
2014-09-26 04:32:07 +00:00
|
|
|
#if TARGET_OS_IPHONE
|
2020-03-05 16:29:10 +00:00
|
|
|
PearlMainQueue( ^{
|
|
|
|
UIAlertController *controller = [UIAlertController alertControllerWithTitle:@"Purchase Failed" message:
|
|
|
|
strf( @"%@\n\n%@", error.localizedDescription,
|
|
|
|
@"Ensure you are online and try logging out and back into iTunes from your device's Settings." )
|
|
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
[controller addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]];
|
|
|
|
[self.navigationController presentViewController:controller animated:YES completion:nil];
|
|
|
|
} );
|
2014-09-26 04:32:07 +00:00
|
|
|
#endif
|
2014-09-21 14:49:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#pragma mark - SKPaymentTransactionObserver
|
|
|
|
|
|
|
|
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
|
|
|
|
|
|
|
|
for (SKPaymentTransaction *transaction in transactions) {
|
|
|
|
switch (transaction.transactionState) {
|
2014-09-26 04:32:07 +00:00
|
|
|
case SKPaymentTransactionStatePurchased: {
|
2017-04-30 03:03:50 +00:00
|
|
|
NSMutableDictionary *attributes = [NSMutableDictionary new];
|
|
|
|
|
2014-09-26 04:32:07 +00:00
|
|
|
if ([transaction.payment.productIdentifier isEqualToString:MPProductFuel]) {
|
2014-09-30 04:01:33 +00:00
|
|
|
float currentFuel = [[MPiOSConfig get].developmentFuelRemaining floatValue];
|
2014-09-26 04:32:07 +00:00
|
|
|
float purchasedFuel = transaction.payment.quantity / MP_FUEL_HOURLY_RATE;
|
2014-09-30 04:01:33 +00:00
|
|
|
[MPiOSConfig get].developmentFuelRemaining = @(currentFuel + purchasedFuel);
|
2015-09-23 04:31:33 +00:00
|
|
|
if (![MPiOSConfig get].developmentFuelChecked || currentFuel < DBL_EPSILON)
|
2014-09-28 14:05:36 +00:00
|
|
|
[MPiOSConfig get].developmentFuelChecked = [NSDate date];
|
2017-04-30 03:03:50 +00:00
|
|
|
[attributes addEntriesFromDictionary:@{
|
|
|
|
@"currentFuel" : @(currentFuel),
|
|
|
|
@"purchasedFuel": @(purchasedFuel),
|
|
|
|
}];
|
2014-09-26 04:32:07 +00:00
|
|
|
}
|
2017-04-30 03:03:50 +00:00
|
|
|
|
2014-09-21 14:49:57 +00:00
|
|
|
[[NSUserDefaults standardUserDefaults] setObject:transaction.transactionIdentifier
|
|
|
|
forKey:transaction.payment.productIdentifier];
|
2014-09-26 04:32:07 +00:00
|
|
|
[queue finishTransaction:transaction];
|
2017-04-30 03:03:50 +00:00
|
|
|
|
|
|
|
if ([[MPConfig get].sendInfo boolValue]) {
|
|
|
|
SKProduct *product = self.products[transaction.payment.productIdentifier];
|
2020-01-27 18:27:10 +00:00
|
|
|
[attributes addEntriesFromDictionary:@{
|
|
|
|
@"id": product.productIdentifier,
|
|
|
|
@"name": product.localizedTitle,
|
|
|
|
@"price": product.price.description,
|
|
|
|
@"currency": [product.priceLocale objectForKey:NSLocaleCurrencyCode],
|
|
|
|
@"state" : @"success",
|
|
|
|
@"quantity": @(transaction.payment.quantity).description,
|
|
|
|
}];
|
|
|
|
[Countly.sharedInstance recordEvent:@"purchase" segmentation:attributes];
|
2017-04-30 03:03:50 +00:00
|
|
|
}
|
2014-09-26 04:32:07 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case SKPaymentTransactionStateRestored: {
|
|
|
|
[[NSUserDefaults standardUserDefaults] setObject:transaction.transactionIdentifier
|
|
|
|
forKey:transaction.payment.productIdentifier];
|
|
|
|
[queue finishTransaction:transaction];
|
2014-09-21 14:49:57 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case SKPaymentTransactionStatePurchasing:
|
|
|
|
case SKPaymentTransactionStateDeferred:
|
|
|
|
break;
|
2014-09-26 04:32:07 +00:00
|
|
|
case SKPaymentTransactionStateFailed:
|
2017-04-30 21:48:03 +00:00
|
|
|
MPError( transaction.error, @"Transaction failed: %@.", transaction.payment.productIdentifier );
|
2014-09-26 04:32:07 +00:00
|
|
|
[queue finishTransaction:transaction];
|
2017-04-30 03:03:50 +00:00
|
|
|
|
|
|
|
if ([[MPConfig get].sendInfo boolValue]) {
|
|
|
|
SKProduct *product = self.products[transaction.payment.productIdentifier];
|
2020-01-27 18:27:10 +00:00
|
|
|
[Countly.sharedInstance recordEvent:@"purchase" segmentation:@{
|
|
|
|
@"id": product.productIdentifier,
|
|
|
|
@"name": product.localizedTitle,
|
|
|
|
@"price": product.price.description,
|
|
|
|
@"currency": [product.priceLocale objectForKey:NSLocaleCurrencyCode],
|
|
|
|
@"state" : @"failed",
|
|
|
|
@"quantity": @(transaction.payment.quantity).description,
|
|
|
|
@"reason" : [transaction.error localizedFailureReason]?: [transaction.error localizedDescription],
|
|
|
|
}];
|
2017-04-30 03:03:50 +00:00
|
|
|
}
|
2014-09-26 04:32:07 +00:00
|
|
|
break;
|
2014-09-21 14:49:57 +00:00
|
|
|
}
|
2014-09-26 04:32:07 +00:00
|
|
|
}
|
2017-04-30 21:48:03 +00:00
|
|
|
|
2014-10-24 04:35:05 +00:00
|
|
|
if (![[NSUserDefaults standardUserDefaults] synchronize])
|
|
|
|
wrn( @"Couldn't synchronize after transaction updates." );
|
2017-04-30 21:48:03 +00:00
|
|
|
|
|
|
|
NSMutableDictionary<NSString *, SKPaymentTransaction *> *allTransactions = [[self transactions] mutableCopy];
|
|
|
|
for (SKPaymentTransaction *transaction in transactions)
|
|
|
|
allTransactions[transaction.payment.productIdentifier] = transaction;
|
|
|
|
for (id<MPInAppDelegate> productObserver in self.productObservers)
|
|
|
|
[productObserver updateWithProducts:self.products transactions:allTransactions];
|
2014-09-21 14:49:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error {
|
|
|
|
|
2017-04-30 03:03:50 +00:00
|
|
|
MPError( error, @"StoreKit restore failed." );
|
2014-09-21 14:49:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@end
|