diff --git a/platform-darwin/External/Pearl b/platform-darwin/External/Pearl index 284cd041..1a3e1ea0 160000 --- a/platform-darwin/External/Pearl +++ b/platform-darwin/External/Pearl @@ -1 +1 @@ -Subproject commit 284cd041f8d48301044cf85a7ac4a9abf030e170 +Subproject commit 1a3e1ea0379fe8d0f1b06ce98cd6394654679682 diff --git a/platform-darwin/Source/MPAppDelegate_InApp.h b/platform-darwin/Source/MPAppDelegate_InApp.h index 1f6d9f00..fc17ab47 100644 --- a/platform-darwin/Source/MPAppDelegate_InApp.h +++ b/platform-darwin/Source/MPAppDelegate_InApp.h @@ -25,17 +25,20 @@ #define MPProductTouchID @"com.lyndir.masterpassword.products.touchid" #define MPProductFuel @"com.lyndir.masterpassword.products.fuel" -#define MP_FUEL_HOURLY_RATE 30.f /* Tier 1 purchases/h ~> USD/h */ +#define MP_FUEL_HOURLY_RATE 40.f /* payment in tier 1 purchases / h (≅ USD / h) */ @protocol MPInAppDelegate -- (void)updateWithProducts:(NSDictionary *)products; -- (void)updateWithTransaction:(SKPaymentTransaction *)transaction; +- (void)updateWithProducts:(NSDictionary *)products + transactions:(NSDictionary *)transactions; @end @interface MPAppDelegate_Shared(InApp) +- (NSDictionary *)products; +- (NSDictionary *)transactions; + - (void)registerProductsObserver:(id)delegate; - (void)removeProductsObserver:(id)delegate; diff --git a/platform-darwin/Source/MPAppDelegate_InApp.m b/platform-darwin/Source/MPAppDelegate_InApp.m index d79cc264..5f228ee3 100644 --- a/platform-darwin/Source/MPAppDelegate_InApp.m +++ b/platform-darwin/Source/MPAppDelegate_InApp.m @@ -27,6 +27,16 @@ 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) @@ -34,7 +44,7 @@ PearlAssociatedObjectProperty( NSMutableArray*, ProductObservers, productObserve [self.productObservers addObject:delegate]; if (self.products) - [delegate updateWithProducts:self.products]; + [delegate updateWithProducts:self.products transactions:[self transactions]]; else [self reloadProducts]; } @@ -108,18 +118,6 @@ PearlAssociatedObjectProperty( NSMutableArray*, ProductObservers, productObserve if (payment) { payment.quantity = quantity; [[self paymentQueue] addPayment:payment]; - - if ([[MPConfig get].sendInfo boolValue]) { -#ifdef CRASHLYTICS - [Answers logAddToCartWithPrice:product.price currency:product.priceLocale.currencyCode itemName:product.localizedTitle - itemType:@"InApp" itemId:product.productIdentifier - customAttributes:nil]; - [Answers logStartCheckoutWithPrice:product.price currency:product.priceLocale.currencyCode itemCount:@(quantity) - customAttributes:@{ - @"products": @[ productIdentifier ], - }]; -#endif - } } return; } @@ -138,7 +136,7 @@ PearlAssociatedObjectProperty( NSMutableArray*, ProductObservers, productObserve self.products = products; for (id productObserver in self.productObservers) - [productObserver updateWithProducts:self.products]; + [productObserver updateWithProducts:self.products transactions:[self transactions]]; } - (void)request:(SKRequest *)request didFailWithError:(NSError *)error { @@ -166,6 +164,7 @@ PearlAssociatedObjectProperty( NSMutableArray*, ProductObservers, productObserve 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 ); @@ -190,9 +189,10 @@ PearlAssociatedObjectProperty( NSMutableArray*, ProductObservers, productObserve if ([[MPConfig get].sendInfo boolValue]) { #ifdef CRASHLYTICS SKProduct *product = self.products[transaction.payment.productIdentifier]; - [Answers logPurchaseWithPrice:product.price currency:product.priceLocale.currencyCode success:@YES - itemName:product.localizedTitle itemType:@"InApp" itemId:product.productIdentifier - customAttributes:attributes]; + 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; @@ -208,28 +208,33 @@ PearlAssociatedObjectProperty( NSMutableArray*, ProductObservers, productObserve case SKPaymentTransactionStateDeferred: break; case SKPaymentTransactionStateFailed: - err( @"Transaction failed: %@, reason: %@", transaction.payment.productIdentifier, [transaction.error fullDescription] ); + 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.currencyCode success:@YES - itemName:product.localizedTitle itemType:@"InApp" itemId:product.productIdentifier + [Answers logPurchaseWithPrice:product.price currency:[product.priceLocale objectForKey:NSLocaleCurrencyCode] + success:@NO itemName:product.localizedTitle itemType:@"InApp" itemId:product.productIdentifier customAttributes:@{ - @"state" : @"Failed", - @"reason": [transaction.error fullDescription], + @"state" : @"Failed", + @"quantity": @(transaction.payment.quantity), + @"reason" : [transaction.error localizedFailureReason]?: [transaction.error localizedDescription], }]; #endif } break; } - - for (id productObserver in self.productObservers) - [productObserver updateWithTransaction:transaction]; } + 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 { diff --git a/platform-darwin/Source/MPAppDelegate_Key.m b/platform-darwin/Source/MPAppDelegate_Key.m index 39b71785..7183b658 100644 --- a/platform-darwin/Source/MPAppDelegate_Key.m +++ b/platform-darwin/Source/MPAppDelegate_Key.m @@ -28,7 +28,7 @@ @implementation MPAppDelegate_Shared(Key) -static NSDictionary *createKeyQuery(MPUserEntity *user, BOOL newItem, MPKeyOrigin *keyOrigin) { +- (NSDictionary *)createKeyQueryforUser:(MPUserEntity *)user origin:(out MPKeyOrigin *)keyOrigin { #if TARGET_OS_IPHONE if (user.touchID && kSecUseAuthenticationUI) { @@ -38,17 +38,17 @@ static NSDictionary *createKeyQuery(MPUserEntity *user, BOOL newItem, MPKeyOrigi CFErrorRef acError = NULL; id accessControl = (__bridge_transfer id)SecAccessControlCreateWithFlags( kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlTouchIDCurrentSet, &acError ); - if (!accessControl || acError) - err( @"Could not use TouchID on this device: %@", acError ); + if (!accessControl) + MPError( (__bridge_transfer NSError *)acError, @"Could not use TouchID on this device." ); else return [PearlKeyChain createQueryForClass:kSecClassGenericPassword attributes:@{ - (__bridge id)kSecAttrService : @"Saved Master Password", - (__bridge id)kSecAttrAccount : user.name?: @"", - (__bridge id)kSecAttrAccessControl : accessControl, - (__bridge id)kSecUseAuthenticationUI : (__bridge id)kSecUseAuthenticationUIAllow, - (__bridge id)kSecUseOperationPrompt : + (__bridge id)kSecAttrService : @"Saved Master Password", + (__bridge id)kSecAttrAccount : user.name?: @"", + (__bridge id)kSecAttrAccessControl : accessControl, + (__bridge id)kSecUseAuthenticationUI: (__bridge id)kSecUseAuthenticationUIAllow, + (__bridge id)kSecUseOperationPrompt : strf( @"Access %@'s master password.", user.name ), } matches:nil]; @@ -60,10 +60,10 @@ static NSDictionary *createKeyQuery(MPUserEntity *user, BOOL newItem, MPKeyOrigi return [PearlKeyChain createQueryForClass:kSecClassGenericPassword attributes:@{ - (__bridge id)kSecAttrService: @"Saved Master Password", - (__bridge id)kSecAttrAccount: user.name?: @"", + (__bridge id)kSecAttrService : @"Saved Master Password", + (__bridge id)kSecAttrAccount : user.name?: @"", #if TARGET_OS_IPHONE - (__bridge id)kSecAttrAccessible : (__bridge id)(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly?: kSecAttrAccessibleWhenUnlockedThisDeviceOnly), + (__bridge id)kSecAttrAccessible: (__bridge id)(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly?: kSecAttrAccessibleWhenUnlockedThisDeviceOnly), #endif } matches:nil]; @@ -72,7 +72,7 @@ static NSDictionary *createKeyQuery(MPUserEntity *user, BOOL newItem, MPKeyOrigi - (MPKey *)loadSavedKeyFor:(MPUserEntity *)user { MPKeyOrigin keyOrigin; - NSDictionary *keyQuery = createKeyQuery( user, NO, &keyOrigin ); + NSDictionary *keyQuery = [self createKeyQueryforUser:user origin:&keyOrigin]; id keyAlgorithm = user.algorithm; MPKey *key = [[MPKey alloc] initForFullName:user.name withKeyResolver:^NSData *(id algorithm) { return ![algorithm isEqual:keyAlgorithm]? nil: @@ -100,7 +100,7 @@ static NSDictionary *createKeyQuery(MPUserEntity *user, BOOL newItem, MPKeyOrigi [self forgetSavedKeyFor:user]; inf( @"Saving key in keychain for user: %@", user.userID ); - [PearlKeyChain addOrUpdateItemForQuery:createKeyQuery( user, YES, nil ) + [PearlKeyChain addOrUpdateItemForQuery:[self createKeyQueryforUser:user origin:nil] withAttributes:@{ (__bridge id)kSecValueData: keyData }]; } } @@ -108,7 +108,7 @@ static NSDictionary *createKeyQuery(MPUserEntity *user, BOOL newItem, MPKeyOrigi - (void)forgetSavedKeyFor:(MPUserEntity *)user { - OSStatus result = [PearlKeyChain deleteItemForQuery:createKeyQuery( user, NO, nil )]; + OSStatus result = [PearlKeyChain deleteItemForQuery:[self createKeyQueryforUser:user origin:nil]]; if (result == noErr) { inf( @"Removed key from keychain for user: %@", user.userID ); diff --git a/platform-darwin/Source/iOS/MPStoreViewController.h b/platform-darwin/Source/iOS/MPStoreViewController.h index e4ce015f..ff772a07 100644 --- a/platform-darwin/Source/iOS/MPStoreViewController.h +++ b/platform-darwin/Source/iOS/MPStoreViewController.h @@ -17,20 +17,11 @@ //============================================================================== #import +#import @class MPStoreProductCell; -@interface MPStoreViewController : PearlMutableStaticTableViewController - -@property(weak, nonatomic) IBOutlet MPStoreProductCell *generateLoginCell; -@property(weak, nonatomic) IBOutlet MPStoreProductCell *generateAnswersCell; -@property(weak, nonatomic) IBOutlet MPStoreProductCell *iOSIntegrationCell; -@property(weak, nonatomic) IBOutlet MPStoreProductCell *touchIDCell; -@property(weak, nonatomic) IBOutlet MPStoreProductCell *fuelCell; -@property(weak, nonatomic) IBOutlet UITableViewCell *loadingCell; -@property(weak, nonatomic) IBOutlet NSLayoutConstraint *fuelMeterConstraint; -@property(weak, nonatomic) IBOutlet UIButton *fuelSpeedButton; -@property(weak, nonatomic) IBOutlet UILabel *fuelStatusLabel; +@interface MPStoreViewController : UITableViewController + (NSString *)latestStoreFeatures; @@ -44,4 +35,17 @@ @property(nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator; @property(nonatomic) IBOutlet UIView *purchasedIndicator; +@property(nonatomic, readonly) SKProduct *product; +@property(nonatomic, readonly) NSInteger quantity; + +- (void)updateWithProduct:(SKProduct *)product transaction:(SKPaymentTransaction *)transaction; + +@end + +@interface MPStoreFuelProductCell : MPStoreProductCell + +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *fuelMeterConstraint; +@property(weak, nonatomic) IBOutlet UIButton *fuelSpeedButton; +@property(weak, nonatomic) IBOutlet UILabel *fuelStatusLabel; + @end diff --git a/platform-darwin/Source/iOS/MPStoreViewController.m b/platform-darwin/Source/iOS/MPStoreViewController.m index 64f28a84..e712bb84 100644 --- a/platform-darwin/Source/iOS/MPStoreViewController.m +++ b/platform-darwin/Source/iOS/MPStoreViewController.m @@ -27,8 +27,9 @@ PearlEnum( MPDevelopmentFuelConsumption, @interface MPStoreViewController() -@property(nonatomic, strong) NSNumberFormatter *currencyFormatter; @property(nonatomic, strong) NSDictionary *products; +@property(nonatomic, strong) NSDictionary *transactions; +@property(nonatomic, strong) NSMutableArray *dataSource; @end @@ -57,14 +58,13 @@ PearlEnum( MPDevelopmentFuelConsumption, [super viewDidLoad]; - self.currencyFormatter = [NSNumberFormatter new]; - self.currencyFormatter.numberStyle = NSNumberFormatterCurrencyStyle; - 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 { @@ -73,50 +73,44 @@ PearlEnum( MPDevelopmentFuelConsumption, self.tableView.contentInset = UIEdgeInsetsMake( 64, 0, 49, 0 ); - [self updateCellsHiding:self.allCellsBySection[0] showing:@[ self.loadingCell ]]; - [self.allCellsBySection[0] enumerateObjectsUsingBlock:^(MPStoreProductCell *cell, NSUInteger idx, BOOL *stop) { - if ([cell isKindOfClass:[MPStoreProductCell class]]) { - cell.purchasedIndicator.visible = NO; - [cell.activityIndicator stopAnimating]; - } - }]; - - PearlAddNotificationObserver( NSUserDefaultsDidChangeNotification, nil, [NSOperationQueue mainQueue], - ^(MPStoreViewController *self, NSNotification *note) { - [self updateProducts]; - [self updateFuel]; - } ); [[MPiOSAppDelegate get] registerProductsObserver:self]; - [self updateFuel]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; - PearlRemoveNotificationObservers(); + [[MPiOSAppDelegate get] removeProductsObserver:self]; } #pragma mark - UITableViewDataSource -- (MPStoreProductCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - MPStoreProductCell *cell = (MPStoreProductCell *)[super tableView:tableView cellForRowAtIndexPath:indexPath]; - if (indexPath.section == 0) - cell.selectionStyle = [[MPiOSAppDelegate get] isFeatureUnlocked:[self productForCell:cell].productIdentifier]? - UITableViewCellSelectionStyleNone: UITableViewCellSelectionStyleDefault; - - if (cell.selectionStyle != UITableViewCellSelectionStyleNone) { - cell.selectedBackgroundView = [[UIView alloc] initWithFrame:cell.bounds]; - cell.selectedBackgroundView.backgroundColor = [UIColor colorWithRGBAHex:0x78DDFB33]; - } - - return cell; + return [self.dataSource count]; } -- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return NO; + 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 @@ -128,14 +122,13 @@ PearlEnum( MPDevelopmentFuelConsumption, - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { - MPStoreProductCell *cell = (MPStoreProductCell *)[self tableView:tableView cellForRowAtIndexPath:indexPath]; - if (cell.selectionStyle == UITableViewCellSelectionStyleNone) - return; + UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath]; + if (cell.selectionStyle != UITableViewCellSelectionStyleNone && [cell isKindOfClass:[MPStoreProductCell class]]) { + MPStoreProductCell *productCell = (MPStoreProductCell *)cell; - SKProduct *product = [self productForCell:cell]; - if (product && ![[MPAppDelegate_Shared get] isFeatureUnlocked:product.productIdentifier]) - [[MPAppDelegate_Shared get] purchaseProductWithIdentifier:product.productIdentifier - quantity:[self quantityForProductIdentifier:product.productIdentifier]]; + 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]; } @@ -146,7 +139,9 @@ PearlEnum( MPDevelopmentFuelConsumption, NSUInteger fuelConsumption = [[MPiOSConfig get].developmentFuelConsumption unsignedIntegerValue]; [MPiOSConfig get].developmentFuelConsumption = @((fuelConsumption + 1) % MPDevelopmentFuelConsumptionCount); - [self updateProducts]; + + [self.tableView updateDataSource:self.dataSource toSections:nil reloadItems:@[ self.products[MPProductFuel] ] + withRowAnimation:UITableViewRowAnimationAutomatic]; } - (IBAction)restorePurchases:(id)sender { @@ -170,36 +165,28 @@ PearlEnum( MPDevelopmentFuelConsumption, #pragma mark - MPInAppDelegate -- (void)updateWithProducts:(NSDictionary *)products { +- (void)updateWithProducts:(NSDictionary *)products + transactions:(NSDictionary *)transactions { self.products = products; + self.transactions = transactions; + NSMutableArray *newDataSource = [NSMutableArray arrayWithCapacity:2]; - [self updateProducts]; -} + // 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; + }]]; -- (void)updateWithTransaction:(SKPaymentTransaction *)transaction { + // Section 1: information cells + [newDataSource addObject:@[ @"MPStoreCellFooter" ]]; - MPStoreProductCell *cell = [self cellForProductIdentifier:transaction.payment.productIdentifier]; - if (!cell) - return; - - switch (transaction.transactionState) { - case SKPaymentTransactionStatePurchasing: - [cell.activityIndicator startAnimating]; - break; - case SKPaymentTransactionStatePurchased: - [cell.activityIndicator stopAnimating]; - break; - case SKPaymentTransactionStateFailed: - [cell.activityIndicator stopAnimating]; - break; - case SKPaymentTransactionStateRestored: - [cell.activityIndicator stopAnimating]; - break; - case SKPaymentTransactionStateDeferred: - [cell.activityIndicator startAnimating]; - break; - } + [self.tableView updateDataSource:self.dataSource toSections:newDataSource + reloadItems:reloadProducts withRowAnimation:UITableViewRowAnimationAutomatic]; } #pragma mark - Private @@ -216,60 +203,63 @@ PearlEnum( MPDevelopmentFuelConsumption, return nil; } -- (SKProduct *)productForCell:(MPStoreProductCell *)cell { +@end - for (SKProduct *product in [self.products allValues]) - if ([self cellForProductIdentifier:product.productIdentifier] == cell) - return product; +@implementation MPStoreProductCell - return nil; +- (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; + + if (transaction && (transaction.transactionState == SKPaymentTransactionStateDeferred || + transaction.transactionState == SKPaymentTransactionStatePurchasing)) + [self.activityIndicator startAnimating]; + else + [self.activityIndicator stopAnimating]; } -- (MPStoreProductCell *)cellForProductIdentifier:(NSString *)productIdentifier { +- (NSString *)price { - if ([productIdentifier isEqualToString:MPProductGenerateLogins]) - return self.generateLoginCell; - if ([productIdentifier isEqualToString:MPProductGenerateAnswers]) - return self.generateAnswersCell; - if ([productIdentifier isEqualToString:MPProductOSIntegration]) - return self.iOSIntegrationCell; - if ([productIdentifier isEqualToString:MPProductTouchID]) - return self.touchIDCell; - if ([productIdentifier isEqualToString:MPProductFuel]) - return self.fuelCell; + NSNumberFormatter *currencyFormatter = [NSNumberFormatter new]; + currencyFormatter.numberStyle = NSNumberFormatterCurrencyStyle; + currencyFormatter.locale = self.product.priceLocale; - return nil; + return [currencyFormatter stringFromNumber:@([self.product.price floatValue] * self.quantity)]; } -- (void)updateProducts { +- (NSInteger)quantity { - NSMutableArray *showCells = [NSMutableArray array]; - NSMutableArray *hideCells = [NSMutableArray array]; - [hideCells addObjectsFromArray:[self.allCellsBySection[0] array]]; - [hideCells addObject:self.loadingCell]; - - for (SKProduct *product in [self.products allValues]) { - [self showCellForProductWithIdentifier:MPProductGenerateLogins ifProduct:product showingCells:showCells]; - [self showCellForProductWithIdentifier:MPProductGenerateAnswers ifProduct:product showingCells:showCells]; - [self showCellForProductWithIdentifier:MPProductOSIntegration ifProduct:product showingCells:showCells]; - [self showCellForProductWithIdentifier:MPProductTouchID ifProduct:product showingCells:showCells]; - [self showCellForProductWithIdentifier:MPProductFuel ifProduct:product showingCells:showCells]; - } - - [hideCells removeObjectsInArray:showCells]; - [self updateCellsHiding:hideCells showing:showCells]; + return 1; } -- (void)updateFuel { +@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]; - NSTimeInterval fuelSecondsElapsed = -[[MPiOSConfig get].developmentFuelChecked timeIntervalSinceDate:now]; - if (fuelSecondsElapsed > 3600 || ![MPiOSConfig get].developmentFuelChecked) { + 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 = weeklyFuelConsumption * weeksElapsed; + NSTimeInterval fuelConsumed = MIN( fuelRemaining, weeklyFuelConsumption * weeksElapsed ); fuelRemaining -= fuelConsumed; fuelInvested += fuelConsumed; [MPiOSConfig get].developmentFuelChecked = now; @@ -277,56 +267,43 @@ PearlEnum( MPDevelopmentFuelConsumption, [MPiOSConfig get].developmentFuelInvested = @(fuelInvested); } - CGFloat fuelRatio = weeklyFuelConsumption == 0? 0: fuelRemaining / weeklyFuelConsumption; /* 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\ninvested: %0.1f work hours", fuelRemaining, 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: - [self.fuelSpeedButton setTitle:@"1h / quarter" forState:UIControlStateNormal]; return 1.f / 12 /* 12 weeks */; case MPDevelopmentFuelConsumptionMonthly: - [self.fuelSpeedButton setTitle:@"1h / month" forState:UIControlStateNormal]; return 1.f / 4 /* 4 weeks */; case MPDevelopmentFuelWeekly: - [self.fuelSpeedButton setTitle:@"1h / week" forState:UIControlStateNormal]; - return 1.f; + return 1.f /* 1 week */; } return 0; } -- (void)showCellForProductWithIdentifier:(NSString *)productIdentifier ifProduct:(SKProduct *)product - showingCells:(NSMutableArray *)showCells { +- (NSString *)weeklyFuelConsumptionTitle { - if (![product.productIdentifier isEqualToString:productIdentifier]) - return; + switch ((MPDevelopmentFuelConsumption)[[MPiOSConfig get].developmentFuelConsumption unsignedIntegerValue]) { + case MPDevelopmentFuelConsumptionQuarterly: + return @"1h / quarter"; + case MPDevelopmentFuelConsumptionMonthly: + return @"1h / month"; + case MPDevelopmentFuelWeekly: + return @"1h / week"; + } - MPStoreProductCell *cell = [self cellForProductIdentifier:productIdentifier]; - [showCells addObject:cell]; - - self.currencyFormatter.locale = product.priceLocale; - BOOL purchased = [[MPiOSAppDelegate get] isFeatureUnlocked:productIdentifier]; - NSInteger quantity = [self quantityForProductIdentifier:productIdentifier]; - cell.priceLabel.text = purchased? @"": [self.currencyFormatter stringFromNumber:@([product.price floatValue] * quantity)]; - cell.titleLabel.text = product.localizedTitle; - cell.descriptionLabel.text = product.localizedDescription; - cell.purchasedIndicator.visible = purchased; -} - -- (NSInteger)quantityForProductIdentifier:(NSString *)productIdentifier { - - if ([productIdentifier isEqualToString:MPProductFuel]) - return (NSInteger)(MP_FUEL_HOURLY_RATE * [self weeklyFuelConsumption] + .5f); - - return 1; + return nil; } @end - -@implementation MPStoreProductCell -@end diff --git a/platform-darwin/Source/iOS/Storyboard.storyboard b/platform-darwin/Source/iOS/Storyboard.storyboard index 77a69134..874beea1 100644 --- a/platform-darwin/Source/iOS/Storyboard.storyboard +++ b/platform-darwin/Source/iOS/Storyboard.storyboard @@ -1039,7 +1039,7 @@ -