327 lines
13 KiB
Objective-C
327 lines
13 KiB
Objective-C
//==============================================================================
|
|
// 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 <http://www.gnu.org/licenses/>.
|
|
//==============================================================================
|
|
|
|
#import "MPStoreViewController.h"
|
|
#import "MPiOSAppDelegate.h"
|
|
#import "UIColor+Expanded.h"
|
|
#import "MPAppDelegate_InApp.h"
|
|
#import "MPPasswordsViewController.h"
|
|
|
|
PearlEnum( MPDevelopmentFuelConsumption,
|
|
MPDevelopmentFuelConsumptionQuarterly, MPDevelopmentFuelConsumptionMonthly, MPDevelopmentFuelWeekly );
|
|
|
|
@interface MPStoreViewController()<MPInAppDelegate>
|
|
|
|
@property(nonatomic, strong) NSDictionary<NSString *, SKProduct *> *products;
|
|
@property(nonatomic, strong) NSDictionary<NSString *, SKPaymentTransaction *> *transactions;
|
|
@property(nonatomic, strong) NSMutableArray<NSArray *> *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<NSString *, SKProduct *> *)products
|
|
transactions:(NSDictionary<NSString *, SKPaymentTransaction *> *)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
|