//============================================================================== // 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 "MPStoreViewController.h" #import "MPiOSAppDelegate.h" #import "UIColor+Expanded.h" #import "MPAppDelegate_InApp.h" #import "MPPasswordsViewController.h" PearlEnum( MPDevelopmentFuelConsumption, MPDevelopmentFuelConsumptionQuarterly, MPDevelopmentFuelConsumptionMonthly, MPDevelopmentFuelWeekly ); @interface MPStoreViewController() @property(nonatomic, strong) NSDictionary *products; @property(nonatomic, strong) NSDictionary *transactions; @property(nonatomic, strong) NSMutableArray *dataSource; @end @implementation MPStoreViewController + (NSString *)latestStoreFeatures { NSMutableString *features = [NSMutableString string]; NSArray *storeVersions = @[ @"Generated Usernames\nSecurity Question Answers", @"TouchID Support" ]; NSInteger storeVersion = [[NSUserDefaults standardUserDefaults] integerForKey:@"storeVersion"]; for (; storeVersion < [storeVersions count]; ++storeVersion) [features appendFormat:@"%@\n", storeVersions[(NSUInteger)storeVersion]]; if (![features length]) return nil; [[NSUserDefaults standardUserDefaults] setInteger:storeVersion forKey:@"storeVersion"]; if (![[NSUserDefaults standardUserDefaults] synchronize]) wrn( @"Couldn't synchronize store version update." ); return features; } - (void)viewDidLoad { [super viewDidLoad]; self.tableView.tableHeaderView = [UIView new]; self.tableView.tableFooterView = [UIView new]; self.tableView.rowHeight = UITableViewAutomaticDimension; self.tableView.estimatedRowHeight = 400; self.view.backgroundColor = [UIColor clearColor]; self.dataSource = [@[ @[], @[ @"MPStoreCellSpinner", @"MPStoreCellFooter" ] ] mutableCopy]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.tableView.contentInset = UIEdgeInsetsMake( 64, 0, 49, 0 ); [[MPiOSAppDelegate get] registerProductsObserver:self]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [[MPiOSAppDelegate get] removeProductsObserver:self]; } #pragma mark - UITableViewDataSource - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [self.dataSource count]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.dataSource[(NSUInteger)section] count]; } - (MPStoreProductCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { id content = self.dataSource[(NSUInteger)indexPath.section][(NSUInteger)indexPath.row]; if ([content isKindOfClass:[SKProduct class]]) { SKProduct *product = content; MPStoreProductCell *cell; if ([product.productIdentifier isEqualToString:MPProductFuel]) cell = [MPStoreFuelProductCell dequeueCellFromTableView:tableView indexPath:indexPath]; else cell = [MPStoreProductCell dequeueCellFromTableView:tableView indexPath:indexPath]; [cell updateWithProduct:product transaction:self.transactions[product.productIdentifier]]; return cell; } return [tableView dequeueReusableCellWithIdentifier:content forIndexPath:indexPath]; } #pragma mark - UITableViewDelegate - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return UITableViewAutomaticDimension; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath]; if (cell.selectionStyle != UITableViewCellSelectionStyleNone && [cell isKindOfClass:[MPStoreProductCell class]]) { MPStoreProductCell *productCell = (MPStoreProductCell *)cell; if (productCell.product && ![[MPAppDelegate_Shared get] isFeatureUnlocked:productCell.product.productIdentifier]) [[MPAppDelegate_Shared get] purchaseProductWithIdentifier:productCell.product.productIdentifier quantity:productCell.quantity]; } [tableView deselectRowAtIndexPath:indexPath animated:YES]; } #pragma mark - Actions - (IBAction)toggleFuelConsumption:(id)sender { NSUInteger fuelConsumption = [[MPiOSConfig get].developmentFuelConsumption unsignedIntegerValue]; [MPiOSConfig get].developmentFuelConsumption = @((fuelConsumption + 1) % MPDevelopmentFuelConsumptionCount); [self.tableView updateDataSource:self.dataSource toSections:nil reloadItems:@[ self.products[MPProductFuel] ] withRowAnimation:UITableViewRowAnimationAutomatic]; } - (IBAction)restorePurchases:(id)sender { [PearlAlert showAlertWithTitle:@"Restore Previous Purchases" message: @"This will check with Apple to find and activate any purchases you made from other devices." viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) { if (buttonIndex == [alert cancelButtonIndex]) return; [[MPAppDelegate_Shared get] restoreCompletedTransactions]; } cancelTitle:@"Cancel" otherTitles:@"Find Purchases", nil]; } - (IBAction)sendThanks:(id)sender { [[self dismissPopup].navigationController performSegueWithIdentifier:@"web" sender: [NSURL URLWithString:@"https://thanks.lhunath.com"]]; } #pragma mark - MPInAppDelegate - (void)updateWithProducts:(NSDictionary *)products transactions:(NSDictionary *)transactions { self.products = products; self.transactions = transactions; NSMutableArray *newDataSource = [NSMutableArray arrayWithCapacity:2]; // Section 0: products [newDataSource addObject:[[products allValues] sortedArrayUsingComparator: ^NSComparisonResult(SKProduct *p1, SKProduct *p2) { return [p1.productIdentifier compare:p2.productIdentifier]; }]]; NSArray *reloadProducts = [newDataSource[0] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock: ^BOOL(SKProduct *product, NSDictionary *bindings) { return self.transactions[product.productIdentifier] != nil; }]]; // Section 1: information cells [newDataSource addObject:@[ @"MPStoreCellFooter" ]]; [self.tableView updateDataSource:self.dataSource toSections:newDataSource reloadItems:reloadProducts withRowAnimation:UITableViewRowAnimationAutomatic]; } #pragma mark - Private - (MPPasswordsViewController *)dismissPopup { for (UIViewController *vc = self; (vc = vc.parentViewController);) if ([vc isKindOfClass:[MPPasswordsViewController class]]) { MPPasswordsViewController *passwordsVC = (MPPasswordsViewController *)vc; [passwordsVC dismissPopdown:self]; return passwordsVC; } return nil; } @end @implementation MPStoreProductCell - (void)updateWithProduct:(SKProduct *)product transaction:(SKPaymentTransaction *)transaction { _product = product; BOOL purchased = [[MPiOSAppDelegate get] isFeatureUnlocked:self.product.productIdentifier]; self.selectionStyle = purchased? UITableViewCellSelectionStyleNone: UITableViewCellSelectionStyleDefault; self.selectedBackgroundView = self.selectionStyle == UITableViewCellSelectionStyleNone? nil: [[UIView alloc] initWithFrame:self.bounds]; self.selectedBackgroundView.backgroundColor = [UIColor colorWithRGBAHex:0x78DDFB33]; self.purchasedIndicator.visible = purchased; self.priceLabel.text = purchased? @"": [self price]; self.titleLabel.text = product.localizedTitle; self.descriptionLabel.text = product.localizedDescription; self.thumbnailView.image = [self productImage]; if (transaction && (transaction.transactionState == SKPaymentTransactionStateDeferred || transaction.transactionState == SKPaymentTransactionStatePurchasing)) [self.activityIndicator startAnimating]; else [self.activityIndicator stopAnimating]; } - (UIImage *)productImage { if ([MPProductGenerateLogins isEqualToString:self.product.productIdentifier]) return [UIImage imageNamed:@"thumb_generated_login"]; if ([MPProductGenerateAnswers isEqualToString:self.product.productIdentifier]) return [UIImage imageNamed:@"thumb_generated_answers"]; if ([MPProductOSIntegration isEqualToString:self.product.productIdentifier]) return [UIImage imageNamed:@"thumb_ios_integration"]; if ([MPProductTouchID isEqualToString:self.product.productIdentifier]) return [UIImage imageNamed:@"thumb_touch_id"]; if ([MPProductFuel isEqualToString:self.product.productIdentifier]) return [UIImage imageNamed:@"thumb_fuel"]; return nil; } - (NSString *)price { NSNumberFormatter *currencyFormatter = [NSNumberFormatter new]; currencyFormatter.numberStyle = NSNumberFormatterCurrencyStyle; currencyFormatter.locale = self.product.priceLocale; return [currencyFormatter stringFromNumber:@([self.product.price floatValue] * self.quantity)]; } - (NSInteger)quantity { return 1; } @end @implementation MPStoreFuelProductCell - (void)updateWithProduct:(SKProduct *)product transaction:(SKPaymentTransaction *)transaction { [super updateWithProduct:product transaction:transaction]; CGFloat weeklyFuelConsumption = [self weeklyFuelConsumption]; /* consume x fuel / week */ [self.fuelSpeedButton setTitle:[self weeklyFuelConsumptionTitle] forState:UIControlStateNormal]; NSTimeInterval fuelSecondsElapsed = 0; CGFloat fuelRemaining = [[MPiOSConfig get].developmentFuelRemaining floatValue]; /* x fuel left */ CGFloat fuelInvested = [[MPiOSConfig get].developmentFuelInvested floatValue]; /* x fuel left */ NSDate *now = [NSDate date], *checked = [MPiOSConfig get].developmentFuelChecked; if (!checked || 3600 < (fuelSecondsElapsed = [now timeIntervalSinceDate:checked])) { NSTimeInterval weeksElapsed = fuelSecondsElapsed / (3600 * 24 * 7 /* 1 week */); /* x weeks elapsed */ NSTimeInterval fuelConsumed = MIN( fuelRemaining, weeklyFuelConsumption * weeksElapsed ); fuelRemaining -= fuelConsumed; fuelInvested += fuelConsumed; [MPiOSConfig get].developmentFuelChecked = now; [MPiOSConfig get].developmentFuelRemaining = @(fuelRemaining); [MPiOSConfig get].developmentFuelInvested = @(fuelInvested); } CGFloat fuelRatio = weeklyFuelConsumption? fuelRemaining / weeklyFuelConsumption: 0; /* x weeks worth of fuel left */ [self.fuelMeterConstraint updateConstant:MIN( 0.5f, fuelRatio - 0.5f ) * 160]; /* -80pt = 0 weeks left, +80pt = >=1 week left */ self.fuelStatusLabel.text = strf( @"Fuel left: %0.1f work hours\nFunded: %0.1f work hours", fuelRemaining, fuelInvested ); self.fuelStatusLabel.hidden = (fuelRemaining + fuelInvested) == 0; } - (NSInteger)quantity { return MAX( 1, (NSInteger)ceil( MP_FUEL_HOURLY_RATE * [self weeklyFuelConsumption] ) ); } - (CGFloat)weeklyFuelConsumption { switch ((MPDevelopmentFuelConsumption)[[MPiOSConfig get].developmentFuelConsumption unsignedIntegerValue]) { case MPDevelopmentFuelConsumptionQuarterly: return 1.f / 12 /* 12 weeks */; case MPDevelopmentFuelConsumptionMonthly: return 1.f / 4 /* 4 weeks */; case MPDevelopmentFuelWeekly: return 1.f /* 1 week */; } return 0; } - (NSString *)weeklyFuelConsumptionTitle { switch ((MPDevelopmentFuelConsumption)[[MPiOSConfig get].developmentFuelConsumption unsignedIntegerValue]) { case MPDevelopmentFuelConsumptionQuarterly: return @"1h / quarter"; case MPDevelopmentFuelConsumptionMonthly: return @"1h / month"; case MPDevelopmentFuelWeekly: return @"1h / week"; } return nil; } @end