//============================================================================== // This file is part of Master Password. // Copyright (c) 2011-2017, Maarten Billemont. // // 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. // // 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. // // You can find a copy of the GNU General Public License in the // LICENSE file. Alternatively, see . //============================================================================== #import "MPAppDelegate_InApp.h" @interface MPAppDelegate_Shared(InApp_Private) @end @implementation MPAppDelegate_Shared(InApp) PearlAssociatedObjectProperty( NSDictionary*, Products, products ); PearlAssociatedObjectProperty( NSMutableArray*, ProductObservers, productObservers ); - (NSDictionary *)transactions { NSMutableDictionary *transactions = [NSMutableDictionary dictionaryWithCapacity:self.paymentQueue.transactions.count]; for (SKPaymentTransaction *transaction in self.paymentQueue.transactions) transactions[transaction.payment.productIdentifier] = transaction; return transactions; } - (void)registerProductsObserver:(id)delegate { if (!self.productObservers) self.productObservers = [NSMutableArray array]; [self.productObservers addObject:delegate]; if (self.products) [delegate updateWithProducts:self.products transactions:[self transactions]]; else [self reloadProducts]; } - (void)removeProductsObserver:(id)delegate { [self.productObservers removeObject:delegate]; } - (void)reloadProducts { SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers: [[NSSet alloc] initWithObjects:MPProductGenerateLogins, MPProductGenerateAnswers, MPProductTouchID, MPProductFuel, nil]]; productsRequest.delegate = self; [productsRequest start]; } - (SKPaymentQueue *)paymentQueue { static dispatch_once_t once = 0; dispatch_once( &once, ^{ [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; } ); return [SKPaymentQueue defaultQueue]; } - (BOOL)canMakePayments { return [SKPaymentQueue canMakePayments]; } - (BOOL)isFeatureUnlocked:(NSString *)productIdentifier { if (![productIdentifier length]) // Missing a product. return NO; if ([productIdentifier isEqualToString:MPProductFuel]) // Consumable product. return NO; #if DEBUG // All features are unlocked for beta / debug / mac versions. return YES; #else // Check if product is purchased. return [[NSUserDefaults standardUserDefaults] objectForKey:productIdentifier] != nil; #endif } - (void)restoreCompletedTransactions { [[self paymentQueue] restoreCompletedTransactions]; } - (void)purchaseProductWithIdentifier:(NSString *)productIdentifier quantity:(NSInteger)quantity { #if TARGET_OS_IPHONE if (![[MPAppDelegate_Shared get] canMakePayments]) { [PearlAlert showAlertWithTitle:@"App Store Not Set Up" message: @"Make sure your Apple ID is set under Settings -> iTunes & App Store, " @"you have a payment method added to the account and purchases are" @"not disabled under General -> Restrictions." viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:nil cancelTitle:@"Thanks" otherTitles:nil]; return; } #endif for (SKProduct *product in [self.products allValues]) if ([product.productIdentifier isEqualToString:productIdentifier]) { SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product]; if (payment) { payment.quantity = quantity; [[self paymentQueue] addPayment:payment]; } return; } } #pragma mark - SKProductsRequestDelegate - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { 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; for (id productObserver in self.productObservers) [productObserver updateWithProducts:self.products transactions:[self transactions]]; } - (void)request:(SKRequest *)request didFailWithError:(NSError *)error { MPError( error, @"StoreKit request (%@) failed.", request ); #if TARGET_OS_IPHONE [PearlAlert showAlertWithTitle:@"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." ) viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:nil cancelTitle:@"OK" otherTitles:nil]; #else #endif } - (void)requestDidFinish:(SKRequest *)request { dbg( @"StoreKit request (%@) finished.", request ); } #pragma mark - SKPaymentTransactionObserver - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction *transaction in transactions) { dbg( @"transaction updated: %@ -> %d", transaction.payment.productIdentifier, (int)(transaction.transactionState) ); switch (transaction.transactionState) { case SKPaymentTransactionStatePurchased: { inf( @"Purchased: %@", transaction.payment.productIdentifier ); NSMutableDictionary *attributes = [NSMutableDictionary new]; if ([transaction.payment.productIdentifier isEqualToString:MPProductFuel]) { float currentFuel = [[MPiOSConfig get].developmentFuelRemaining floatValue]; float purchasedFuel = transaction.payment.quantity / MP_FUEL_HOURLY_RATE; [MPiOSConfig get].developmentFuelRemaining = @(currentFuel + purchasedFuel); if (![MPiOSConfig get].developmentFuelChecked || currentFuel < DBL_EPSILON) [MPiOSConfig get].developmentFuelChecked = [NSDate date]; [attributes addEntriesFromDictionary:@{ @"currentFuel" : @(currentFuel), @"purchasedFuel": @(purchasedFuel), }]; } [[NSUserDefaults standardUserDefaults] setObject:transaction.transactionIdentifier forKey:transaction.payment.productIdentifier]; [queue finishTransaction:transaction]; if ([[MPConfig get].sendInfo boolValue]) { #ifdef CRASHLYTICS SKProduct *product = self.products[transaction.payment.productIdentifier]; for (int q = 0; q < transaction.payment.quantity; ++q) [Answers logPurchaseWithPrice:product.price currency:[product.priceLocale objectForKey:NSLocaleCurrencyCode] success:@YES itemName:product.localizedTitle itemType:@"InApp" itemId:product.productIdentifier customAttributes:attributes]; #endif } break; } case SKPaymentTransactionStateRestored: { inf( @"Restored: %@", transaction.payment.productIdentifier ); [[NSUserDefaults standardUserDefaults] setObject:transaction.transactionIdentifier forKey:transaction.payment.productIdentifier]; [queue finishTransaction:transaction]; break; } case SKPaymentTransactionStatePurchasing: case SKPaymentTransactionStateDeferred: break; case SKPaymentTransactionStateFailed: MPError( transaction.error, @"Transaction failed: %@.", transaction.payment.productIdentifier ); [queue finishTransaction:transaction]; if ([[MPConfig get].sendInfo boolValue]) { #ifdef CRASHLYTICS SKProduct *product = self.products[transaction.payment.productIdentifier]; [Answers logPurchaseWithPrice:product.price currency:[product.priceLocale objectForKey:NSLocaleCurrencyCode] success:@NO itemName:product.localizedTitle itemType:@"InApp" itemId:product.productIdentifier customAttributes:@{ @"state" : @"Failed", @"quantity": @(transaction.payment.quantity), @"reason" : [transaction.error localizedFailureReason]?: [transaction.error localizedDescription], }]; #endif } break; } } if (![[NSUserDefaults standardUserDefaults] synchronize]) wrn( @"Couldn't synchronize after transaction updates." ); NSMutableDictionary *allTransactions = [[self transactions] mutableCopy]; for (SKPaymentTransaction *transaction in transactions) allTransactions[transaction.payment.productIdentifier] = transaction; for (id productObserver in self.productObservers) [productObserver updateWithProducts:self.products transactions:allTransactions]; } - (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error { MPError( error, @"StoreKit restore failed." ); } @end