From 6d9be3fdfeaa468042c78655b56ecca2a34d709c Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sat, 29 Apr 2017 23:03:50 -0400 Subject: [PATCH] Add support for Answers and improved Fabric integration. --- .../project.pbxproj | 48 +++++++------ .../project.pbxproj | 32 ++++----- .../Crashlytics.plist => Fabric/Fabric.plist} | 0 platform-darwin/Scripts/updatePlist | 6 +- platform-darwin/Source/MPAlgorithmV0.m | 2 +- platform-darwin/Source/MPAppDelegate_InApp.h | 2 +- platform-darwin/Source/MPAppDelegate_InApp.m | 69 ++++++++++++++++--- platform-darwin/Source/MPAppDelegate_Key.m | 16 ++++- platform-darwin/Source/MPAppDelegate_Shared.h | 6 +- platform-darwin/Source/MPAppDelegate_Shared.m | 9 +-- platform-darwin/Source/MPAppDelegate_Store.m | 38 +++++----- platform-darwin/Source/MPConfig.m | 2 +- platform-darwin/Source/MPEntities.m | 4 +- platform-darwin/Source/MPTypes.h | 20 ++++++ platform-darwin/Source/Mac/MPMacAppDelegate.m | 36 +++++----- .../Source/Mac/MPPasswordWindowController.m | 2 +- platform-darwin/Source/Mac/MPSiteModel.m | 2 +- .../Source/iOS/MPAnswersViewController.m | 12 ++-- .../Source/iOS/MPPasswordsViewController.m | 2 +- .../Source/iOS/MPStoreViewController.h | 2 + .../Source/iOS/MPStoreViewController.m | 12 ++-- .../Source/iOS/MPUsersViewController.m | 14 +++- platform-darwin/Source/iOS/MPiOSAppDelegate.m | 14 ++-- .../Source/iOS/MasterPassword-Info.plist | 2 +- .../Source/iOS/Settings.bundle/Root.plist | 4 +- .../Source/iOS/Storyboard.storyboard | 10 +++ 26 files changed, 237 insertions(+), 129 deletions(-) rename platform-darwin/Resources/{Crashlytics/Crashlytics.plist => Fabric/Fabric.plist} (100%) diff --git a/platform-darwin/MasterPassword-iOS.xcodeproj/project.pbxproj b/platform-darwin/MasterPassword-iOS.xcodeproj/project.pbxproj index 6f179af3..81dc63c8 100644 --- a/platform-darwin/MasterPassword-iOS.xcodeproj/project.pbxproj +++ b/platform-darwin/MasterPassword-iOS.xcodeproj/project.pbxproj @@ -42,7 +42,6 @@ 93D398ECD7D1A0DEDDADF516 /* MPEmergencyViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39ACBA9F4878B6A1CC33B /* MPEmergencyViewController.m */; }; 93D399246DC90F50913A1287 /* UIResponder+PearlFirstResponder.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39A1DDFA09AE2E14D26DC /* UIResponder+PearlFirstResponder.m */; }; 93D3992FA1546E01F498F665 /* PearlNavigationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 93D398567FD02DB2647B8CF3 /* PearlNavigationController.h */; }; - 93D399433EA75E50656040CB /* Twitter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93D394077F8FAB8167647187 /* Twitter.framework */; }; 93D39943D01E70DAC3B0DF76 /* mpw-util.c in Sources */ = {isa = PBXBuildFile; fileRef = 93D396C311C3725870343EE0 /* mpw-util.c */; }; 93D399D7E08A142776A74CB8 /* MPOverlayViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D395105935859D71679931 /* MPOverlayViewController.m */; }; 93D399E4BC1E092A8C8B12AE /* NSOrderedSetOrArray.h in Headers */ = {isa = PBXBuildFile; fileRef = 93D39FBF8FCEB4C106272334 /* NSOrderedSetOrArray.h */; }; @@ -87,6 +86,9 @@ DA0CC5361EAB99BA009A8ED9 /* IASKTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = DA0CC5241EAB99BA009A8ED9 /* IASKTextField.m */; }; DA0CC5371EAB99BA009A8ED9 /* IASKTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = DA0CC5261EAB99BA009A8ED9 /* IASKTextView.m */; }; DA0CC5381EAB99BA009A8ED9 /* IASKTextViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = DA0CC5281EAB99BA009A8ED9 /* IASKTextViewCell.m */; }; + DA0CC53B1EB57B5C009A8ED9 /* Fabric.plist in Resources */ = {isa = PBXBuildFile; fileRef = DA0CC53A1EB57B5C009A8ED9 /* Fabric.plist */; }; + DA0CC5411EB57BD4009A8ED9 /* Fabric.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA0CC53F1EB57B91009A8ED9 /* Fabric.framework */; }; + DA0CC5421EB57BD4009A8ED9 /* Crashlytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DAA141191922FED80032B392 /* Crashlytics.framework */; }; DA24EBAE19DAD08900FF010B /* tip_basic_black_top.png in Resources */ = {isa = PBXBuildFile; fileRef = DABD38941711E29700CF925C /* tip_basic_black_top.png */; }; DA24EBAF19DAD08C00FF010B /* tip_basic_black_top@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DABD38951711E29700CF925C /* tip_basic_black_top@2x.png */; }; DA24EBE819DAD6DE00FF010B /* Icon-320.png in Resources */ = {isa = PBXBuildFile; fileRef = DA24EBE619DAD6DE00FF010B /* Icon-320.png */; }; @@ -114,7 +116,6 @@ DA29993219C9132F00AF7DF1 /* thumb_generated_login@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = DA29993119C9132F00AF7DF1 /* thumb_generated_login@3x.png */; }; DA29993319C9214600AF7DF1 /* icon_star-hollow.png in Resources */ = {isa = PBXBuildFile; fileRef = DABD382A1711E29600CF925C /* icon_star-hollow.png */; }; DA29993419C9214600AF7DF1 /* icon_star-hollow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DABD382B1711E29600CF925C /* icon_star-hollow@2x.png */; }; - DA2C3D611BD95EEE001137B3 /* Fabric.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA2C3D601BD95EEE001137B3 /* Fabric.framework */; }; DA2C3D631BD96126001137B3 /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = DA2C3D621BD96126001137B3 /* libc++.tbd */; }; DA2C3D651BD9612F001137B3 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = DA2C3D641BD9612F001137B3 /* libz.tbd */; }; DA2CA4DD18D28859007798F8 /* NSArray+Pearl.m in Sources */ = {isa = PBXBuildFile; fileRef = DA2CA4D918D28859007798F8 /* NSArray+Pearl.m */; }; @@ -165,7 +166,6 @@ DA45224A190628A1008F650A /* icon_wrench@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DABD386B1711E29700CF925C /* icon_wrench@2x.png */; }; DA45224B190628B2008F650A /* icon_gear.png in Resources */ = {isa = PBXBuildFile; fileRef = DABD37821711E29500CF925C /* icon_gear.png */; }; DA45224C190628B2008F650A /* icon_gear@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DABD37831711E29500CF925C /* icon_gear@2x.png */; }; - DA48856019A5A82E000C2D79 /* Crashlytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DAA141191922FED80032B392 /* Crashlytics.framework */; }; DA4DA1D91564471A00F6F596 /* libjrswizzle.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DAC6326C148680650075AEA5 /* libjrswizzle.a */; }; DA4DA1DA1564471F00F6F596 /* libuicolor-utilities.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DAC6325D1486805C0075AEA5 /* libuicolor-utilities.a */; }; DA5A09DF171A70E4005284AB /* play.png in Resources */ = {isa = PBXBuildFile; fileRef = DA5A09DD171A70E4005284AB /* play.png */; }; @@ -335,7 +335,6 @@ DAC77CAE148291A600BCF976 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA5BFA4A147E415C00F98B1E /* Foundation.framework */; }; DAC8DF47192831E100BA7D71 /* icon_key.png in Resources */ = {isa = PBXBuildFile; fileRef = DABD379A1711E29600CF925C /* icon_key.png */; }; DAC8DF48192831E100BA7D71 /* icon_key@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DABD379B1711E29600CF925C /* icon_key@2x.png */; }; - DACA296F1705DF81002C6C22 /* Crashlytics.plist in Resources */ = {isa = PBXBuildFile; fileRef = DACA269A1705DF81002C6C22 /* Crashlytics.plist */; }; DACA29731705E1A8002C6C22 /* ciphers.plist in Resources */ = {isa = PBXBuildFile; fileRef = DACA29711705E1A8002C6C22 /* ciphers.plist */; }; DACA29741705E1A8002C6C22 /* dictionary.lst in Resources */ = {isa = PBXBuildFile; fileRef = DACA29721705E1A8002C6C22 /* dictionary.lst */; }; DACA298D1705E2BD002C6C22 /* JRSwizzle.h in Headers */ = {isa = PBXBuildFile; fileRef = DACA29771705E2BD002C6C22 /* JRSwizzle.h */; }; @@ -667,6 +666,8 @@ DA0CC5261EAB99BA009A8ED9 /* IASKTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IASKTextView.m; sourceTree = ""; }; DA0CC5271EAB99BA009A8ED9 /* IASKTextViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IASKTextViewCell.h; sourceTree = ""; }; DA0CC5281EAB99BA009A8ED9 /* IASKTextViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IASKTextViewCell.m; sourceTree = ""; }; + DA0CC53A1EB57B5C009A8ED9 /* Fabric.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Fabric.plist; sourceTree = ""; }; + DA0CC53F1EB57B91009A8ED9 /* Fabric.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Fabric.framework; sourceTree = ""; }; DA24EBB219DAD4D000FF010B /* Icon-60.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Icon-60.png"; sourceTree = ""; }; DA24EBB319DAD4D000FF010B /* Icon-60@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Icon-60@2x.png"; sourceTree = ""; }; DA24EBB419DAD4D000FF010B /* Icon-60@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Icon-60@3x.png"; sourceTree = ""; }; @@ -1496,7 +1497,6 @@ DAC632871486D95D0075AEA5 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; DAC77CAD148291A600BCF976 /* libPearl.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPearl.a; sourceTree = BUILT_PRODUCTS_DIR; }; DAC77CB1148291A600BCF976 /* Pearl-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Pearl-Prefix.pch"; path = "../../Source/Pearl/Pearl-Prefix.pch"; sourceTree = ""; }; - DACA269A1705DF81002C6C22 /* Crashlytics.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Crashlytics.plist; sourceTree = ""; }; DACA29711705E1A8002C6C22 /* ciphers.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = ciphers.plist; sourceTree = ""; }; DACA29721705E1A8002C6C22 /* dictionary.lst */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = dictionary.lst; sourceTree = ""; }; DACA29771705E2BD002C6C22 /* JRSwizzle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JRSwizzle.h; sourceTree = ""; }; @@ -1623,6 +1623,7 @@ DA2C3D651BD9612F001137B3 /* libz.tbd in Frameworks */, DA2C3D631BD96126001137B3 /* libc++.tbd in Frameworks */, DAA1761B19D86D0D0044227B /* libAttributedMarkdown.a in Frameworks */, + DA0CC5421EB57BD4009A8ED9 /* Crashlytics.framework in Frameworks */, DA32D03E19D11293004F3F0E /* libKCOrderedAccessorFix.a in Frameworks */, DA04E33E14B1E70400ECA4F3 /* MobileCoreServices.framework in Frameworks */, DAE2725A19C93B8E007C5262 /* StoreKit.framework in Frameworks */, @@ -1634,15 +1635,13 @@ DA672D3014F9413D004A189C /* libPearl.a in Frameworks */, DAEBC45314F6364500987BF6 /* QuartzCore.framework in Frameworks */, DA95D5F214DF0B2C008D1B94 /* MessageUI.framework in Frameworks */, - DA48856019A5A82E000C2D79 /* Crashlytics.framework in Frameworks */, DAC632891486D9690075AEA5 /* Security.framework in Frameworks */, DA5BFA49147E415C00F98B1E /* UIKit.framework in Frameworks */, DA0979171E9A81EE00F0BFE8 /* libsodium.a in Frameworks */, DA5BFA4B147E415C00F98B1E /* Foundation.framework in Frameworks */, DA5BFA4D147E415C00F98B1E /* CoreGraphics.framework in Frameworks */, DA5BFA4F147E415C00F98B1E /* CoreData.framework in Frameworks */, - 93D399433EA75E50656040CB /* Twitter.framework in Frameworks */, - DA2C3D611BD95EEE001137B3 /* Fabric.framework in Frameworks */, + DA0CC5411EB57BD4009A8ED9 /* Fabric.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1861,6 +1860,14 @@ path = Views; sourceTree = ""; }; + DA0CC5391EB57B5C009A8ED9 /* Fabric */ = { + isa = PBXGroup; + children = ( + DA0CC53A1EB57B5C009A8ED9 /* Fabric.plist */, + ); + path = Fabric; + sourceTree = ""; + }; DA24EBB019DAD4D000FF010B /* ios */ = { isa = PBXGroup; children = ( @@ -2022,6 +2029,7 @@ DAA141181922FED80032B392 /* iOS */ = { isa = PBXGroup; children = ( + DA0CC53F1EB57B91009A8ED9 /* Fabric.framework */, DAA141191922FED80032B392 /* Crashlytics.framework */, ); path = iOS; @@ -2908,7 +2916,7 @@ DACA23B41705DF7D002C6C22 /* Resources */ = { isa = PBXGroup; children = ( - DACA26991705DF81002C6C22 /* Crashlytics */, + DA0CC5391EB57B5C009A8ED9 /* Fabric */, DACA29701705E1A8002C6C22 /* Data */, DAE1EF2417E942DE00BC0086 /* Localizable.strings */, DABD360D1711E29400CF925C /* Media */, @@ -2916,14 +2924,6 @@ path = Resources; sourceTree = ""; }; - DACA26991705DF81002C6C22 /* Crashlytics */ = { - isa = PBXGroup; - children = ( - DACA269A1705DF81002C6C22 /* Crashlytics.plist */, - ); - path = Crashlytics; - sourceTree = ""; - }; DACA29701705E1A8002C6C22 /* Data */ = { isa = PBXGroup; children = ( @@ -3287,7 +3287,7 @@ DA5BFA41147E415C00F98B1E /* Frameworks */, DA5BFA42147E415C00F98B1E /* Resources */, DA6556E314D55F3000841C99 /* Run Script: GIT version -> Info.plist */, - DAD3125D155288AA00A3F9ED /* Run Script: Crashlytics */, + DAD3125D155288AA00A3F9ED /* Run Script: Fabric */, ); buildRules = ( ); @@ -3548,7 +3548,7 @@ buildActionMask = 2147483647; files = ( DAFE4A5A1503982E003ABA7C /* Pearl.strings in Resources */, - DACA296F1705DF81002C6C22 /* Crashlytics.plist in Resources */, + DA0CC53B1EB57B5C009A8ED9 /* Fabric.plist in Resources */, DACA29731705E1A8002C6C22 /* ciphers.plist in Resources */, DA32D04F19D2F59B004F3F0E /* meter_fuel@2x.png in Resources */, DACA29741705E1A8002C6C22 /* dictionary.lst in Resources */, @@ -3778,19 +3778,19 @@ shellScript = "exec Scripts/genassets"; showEnvVarsInLog = 0; }; - DAD3125D155288AA00A3F9ED /* Run Script: Crashlytics */ = { + DAD3125D155288AA00A3F9ED /* Run Script: Fabric */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Run Script: Crashlytics"; + name = "Run Script: Fabric"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = "/bin/bash -e"; - shellScript = "[[ $DEPLOYMENT_LOCATION != YES ]] && exit\n\napiKey=$(/usr/libexec/PlistBuddy -c \"Print :'API Key'\" Resources/Crashlytics/Crashlytics.plist)\n[[ $apiKey ]] && External/iOS/Crashlytics.framework/run \"$apiKey\""; + shellScript = "[[ $DEPLOYMENT_LOCATION != YES ]] && exit\n\napiKey=$(/usr/libexec/PlistBuddy -c \"Print :'API Key'\" Resources/Fabric/Fabric.plist)\n[[ $apiKey ]] && External/iOS/Fabric.framework/run \"$apiKey\""; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -4147,7 +4147,6 @@ ); GCC_PREFIX_HEADER = "Source/MasterPassword-Prefix.pch"; INFOPLIST_FILE = "Source/iOS/MasterPassword-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/External/libsodium/libsodium-ios/lib", @@ -4263,6 +4262,7 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", + "CRASHLYTICS=1", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; @@ -4412,7 +4412,6 @@ ); GCC_PREFIX_HEADER = "Source/MasterPassword-Prefix.pch"; INFOPLIST_FILE = "Source/iOS/MasterPassword-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/External/libsodium/libsodium-ios/lib", @@ -4440,7 +4439,6 @@ ); GCC_PREFIX_HEADER = "Source/MasterPassword-Prefix.pch"; INFOPLIST_FILE = "Source/iOS/MasterPassword-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/External/libsodium/libsodium-ios/lib", diff --git a/platform-darwin/MasterPassword-macOS.xcodeproj/project.pbxproj b/platform-darwin/MasterPassword-macOS.xcodeproj/project.pbxproj index 94514e20..c177ffac 100644 --- a/platform-darwin/MasterPassword-macOS.xcodeproj/project.pbxproj +++ b/platform-darwin/MasterPassword-macOS.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ DA09745B1E99582900F0BFE8 /* mpw-tests.c in Sources */ = {isa = PBXBuildFile; fileRef = DA0974571E99582200F0BFE8 /* mpw-tests.c */; }; DA09745E1E99586600F0BFE8 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = DA09745D1E99586600F0BFE8 /* libxml2.tbd */; }; DA0979681E9A834C00F0BFE8 /* libsodium.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DA0979571E9A824700F0BFE8 /* libsodium.a */; }; + DA0CC53E1EB57B69009A8ED9 /* Fabric.plist in Resources */ = {isa = PBXBuildFile; fileRef = DA0CC53D1EB57B69009A8ED9 /* Fabric.plist */; }; DA1000801998A4C6002B873F /* openssl in Headers */ = {isa = PBXBuildFile; fileRef = DAE8E65719867AF500416A0F /* openssl */; settings = {ATTRIBUTES = (Public, ); }; }; DA16B341170661DB000A0EAB /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA16B340170661DB000A0EAB /* Carbon.framework */; }; DA16B342170661E0000A0EAB /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DAC632871486D95D0075AEA5 /* Security.framework */; }; @@ -148,7 +149,6 @@ DACA27381705DF81002C6C22 /* menu-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = DACA24581705DF7D002C6C22 /* menu-icon.png */; }; DACA29671705DF81002C6C22 /* SourceCodePro-ExtraLight.otf in Resources */ = {isa = PBXBuildFile; fileRef = DACA268E1705DF81002C6C22 /* SourceCodePro-ExtraLight.otf */; }; DACA29681705DF81002C6C22 /* SourceCodePro-Black.otf in Resources */ = {isa = PBXBuildFile; fileRef = DACA268F1705DF81002C6C22 /* SourceCodePro-Black.otf */; }; - DACA296F1705DF81002C6C22 /* Crashlytics.plist in Resources */ = {isa = PBXBuildFile; fileRef = DACA269A1705DF81002C6C22 /* Crashlytics.plist */; }; DACA29731705E1A8002C6C22 /* ciphers.plist in Resources */ = {isa = PBXBuildFile; fileRef = DACA29711705E1A8002C6C22 /* ciphers.plist */; }; DACA29741705E1A8002C6C22 /* dictionary.lst in Resources */ = {isa = PBXBuildFile; fileRef = DACA29721705E1A8002C6C22 /* dictionary.lst */; }; DACA298D1705E2BD002C6C22 /* JRSwizzle.h in Headers */ = {isa = PBXBuildFile; fileRef = DACA29771705E2BD002C6C22 /* JRSwizzle.h */; }; @@ -350,6 +350,7 @@ DA0979531E9A824700F0BFE8 /* version.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = version.h; sourceTree = ""; }; DA0979541E9A824700F0BFE8 /* sodium.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = sodium.h; sourceTree = ""; }; DA0979571E9A824700F0BFE8 /* libsodium.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libsodium.a; sourceTree = ""; }; + DA0CC53D1EB57B69009A8ED9 /* Fabric.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Fabric.plist; sourceTree = ""; }; DA16B340170661DB000A0EAB /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; DA16B343170661EE000A0EAB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; DA2508F019511D3600AC23F1 /* MPPasswordWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MPPasswordWindowController.xib; sourceTree = ""; }; @@ -911,7 +912,6 @@ DACA24581705DF7D002C6C22 /* menu-icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menu-icon.png"; sourceTree = ""; }; DACA268E1705DF81002C6C22 /* SourceCodePro-ExtraLight.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SourceCodePro-ExtraLight.otf"; sourceTree = ""; }; DACA268F1705DF81002C6C22 /* SourceCodePro-Black.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SourceCodePro-Black.otf"; sourceTree = ""; }; - DACA269A1705DF81002C6C22 /* Crashlytics.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Crashlytics.plist; sourceTree = ""; }; DACA29711705E1A8002C6C22 /* ciphers.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = ciphers.plist; sourceTree = ""; }; DACA29721705E1A8002C6C22 /* dictionary.lst */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = dictionary.lst; sourceTree = ""; }; DACA29771705E2BD002C6C22 /* JRSwizzle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JRSwizzle.h; sourceTree = ""; }; @@ -1132,6 +1132,14 @@ path = lib; sourceTree = ""; }; + DA0CC53C1EB57B69009A8ED9 /* Fabric */ = { + isa = PBXGroup; + children = ( + DA0CC53D1EB57B69009A8ED9 /* Fabric.plist */, + ); + path = Fabric; + sourceTree = ""; + }; DA2508F819513C1400AC23F1 /* Other Frameworks */ = { isa = PBXGroup; children = ( @@ -1772,8 +1780,8 @@ DACA23B41705DF7D002C6C22 /* Resources */ = { isa = PBXGroup; children = ( + DA0CC53C1EB57B69009A8ED9 /* Fabric */, DA09745F1E995EB500F0BFE8 /* mpw_tests.xml */, - DACA26991705DF81002C6C22 /* Crashlytics */, DACA29701705E1A8002C6C22 /* Data */, DACA23B51705DF7D002C6C22 /* Media */, ); @@ -1856,14 +1864,6 @@ path = Fonts; sourceTree = ""; }; - DACA26991705DF81002C6C22 /* Crashlytics */ = { - isa = PBXGroup; - children = ( - DACA269A1705DF81002C6C22 /* Crashlytics.plist */, - ); - path = Crashlytics; - sourceTree = ""; - }; DACA29701705E1A8002C6C22 /* Data */ = { isa = PBXGroup; children = ( @@ -2086,7 +2086,7 @@ DA5BFA42147E415C00F98B1E /* Resources */, DAD9B5EE1762CA3A001835F9 /* Copy LoginHelper */, DA6556E314D55F3000841C99 /* Run Script: GIT version -> Info.plist */, - DAD3125D155288AA00A3F9ED /* Run Script: Crashlytics */, + DAD3125D155288AA00A3F9ED /* Run Script: Fabric */, ); buildRules = ( ); @@ -2295,13 +2295,13 @@ DACA27331705DF81002C6C22 /* avatar-12@2x.png in Resources */, DACA27341705DF81002C6C22 /* avatar-2@2x.png in Resources */, DA60717D195D040500CA98B5 /* icon_gear@2x.png in Resources */, + DA0CC53E1EB57B69009A8ED9 /* Fabric.plist in Resources */, DACA27351705DF81002C6C22 /* avatar-11.png in Resources */, DACA27361705DF81002C6C22 /* avatar-0@2x.png in Resources */, DACA27371705DF81002C6C22 /* avatar-10@2x.png in Resources */, DACA27381705DF81002C6C22 /* menu-icon.png in Resources */, DACA29671705DF81002C6C22 /* SourceCodePro-ExtraLight.otf in Resources */, DACA29681705DF81002C6C22 /* SourceCodePro-Black.otf in Resources */, - DACA296F1705DF81002C6C22 /* Crashlytics.plist in Resources */, DACA29731705E1A8002C6C22 /* ciphers.plist in Resources */, DACA29741705E1A8002C6C22 /* dictionary.lst in Resources */, DA5E5D081724A667003798D8 /* MasterPassword.entitlements in Resources */, @@ -2359,19 +2359,19 @@ shellPath = "/bin/sh -e"; shellScript = "exec Scripts/updatePlist"; }; - DAD3125D155288AA00A3F9ED /* Run Script: Crashlytics */ = { + DAD3125D155288AA00A3F9ED /* Run Script: Fabric */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Run Script: Crashlytics"; + name = "Run Script: Fabric"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = "/bin/sh -e"; - shellScript = "[[ $DEPLOYMENT_LOCATION != YES ]] && exit\n\napiKey=$(/usr/libexec/PlistBuddy -c \"Print :'API Key'\" Resources/Crashlytics/Crashlytics.plist)\n[[ $apiKey ]] && External/Mac/Fabric.framework/run \"$apiKey\" 410fb41450e3a2e50fa8357682d812ecd3e1846f2141a99bdb9d3a6a981ad69c"; + shellScript = "[[ $DEPLOYMENT_LOCATION != YES ]] && exit\n\napiKey=$(/usr/libexec/PlistBuddy -c \"Print :'API Key'\" Resources/Fabric/Fabric.plist)\n[[ $apiKey ]] && External/Mac/Fabric.framework/run \"$apiKey\" 410fb41450e3a2e50fa8357682d812ecd3e1846f2141a99bdb9d3a6a981ad69c"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ diff --git a/platform-darwin/Resources/Crashlytics/Crashlytics.plist b/platform-darwin/Resources/Fabric/Fabric.plist similarity index 100% rename from platform-darwin/Resources/Crashlytics/Crashlytics.plist rename to platform-darwin/Resources/Fabric/Fabric.plist diff --git a/platform-darwin/Scripts/updatePlist b/platform-darwin/Scripts/updatePlist index 644997ad..3fa26f5e 100755 --- a/platform-darwin/Scripts/updatePlist +++ b/platform-darwin/Scripts/updatePlist @@ -55,14 +55,14 @@ setSettingWithTitle "Copyright" "$(getPlistWithKey NSHumanReadableCopyright)" if [[ $DEPLOYMENT_LOCATION = YES ]]; then # This build is a release. Do some release checks. - crashlyticsPlist="$BUILT_PRODUCTS_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/Crashlytics.plist" + fabricPlist="$BUILT_PRODUCTS_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/Fabric.plist" passed=1 [[ $description != *-dirty ]] || \ { passed=0; err 'ERROR: Cannot release a dirty version, first commit any changes.'; } [[ $build == 0 ]] || \ { passed=0; err 'ERROR: Commit is not tagged for release, first tag accordingly.'; } - [[ -r "$crashlyticsPlist" && $(PlistBuddy -c "Print :'API Key'" "$crashlyticsPlist" 2>/dev/null) ]] || \ - { passed=0; err 'ERROR: Cannot release: Crashlytics API key is missing.'; } + [[ -r "$fabricPlist" && $(PlistBuddy -c "Print :'API Key'" "$fabricPlist" 2>/dev/null) ]] || \ + { passed=0; err 'ERROR: Cannot release: Fabric API key is missing.'; } (( passed )) || \ { ftl "Failed to pass release checks. Fix the above errors and re-try. Aborting."; exit 1; } fi diff --git a/platform-darwin/Source/MPAlgorithmV0.m b/platform-darwin/Source/MPAlgorithmV0.m index bca243c8..e19057c3 100644 --- a/platform-darwin/Source/MPAlgorithmV0.m +++ b/platform-darwin/Source/MPAlgorithmV0.m @@ -93,7 +93,7 @@ NSOperationQueue *_mpwQueue = nil; migrationRequest.predicate = [NSPredicate predicateWithFormat:@"version_ < %d AND user == %@", self.version, user]; NSArray *migrationSites = [moc executeFetchRequest:migrationRequest error:&error]; if (!migrationSites) { - err( @"While looking for sites to migrate: %@", [error fullDescription] ); + MPError( error, @"While looking for sites to migrate." ); return NO; } diff --git a/platform-darwin/Source/MPAppDelegate_InApp.h b/platform-darwin/Source/MPAppDelegate_InApp.h index 62d14a0f..1f6d9f00 100644 --- a/platform-darwin/Source/MPAppDelegate_InApp.h +++ b/platform-darwin/Source/MPAppDelegate_InApp.h @@ -29,7 +29,7 @@ @protocol MPInAppDelegate -- (void)updateWithProducts:(NSArray /* SKProduct */ *)products; +- (void)updateWithProducts:(NSDictionary *)products; - (void)updateWithTransaction:(SKPaymentTransaction *)transaction; @end diff --git a/platform-darwin/Source/MPAppDelegate_InApp.m b/platform-darwin/Source/MPAppDelegate_InApp.m index 41f78b8b..d79cc264 100644 --- a/platform-darwin/Source/MPAppDelegate_InApp.m +++ b/platform-darwin/Source/MPAppDelegate_InApp.m @@ -23,7 +23,8 @@ @implementation MPAppDelegate_Shared(InApp) -PearlAssociatedObjectProperty( NSArray*, Products, products ); +PearlAssociatedObjectProperty( NSDictionary*, Products, products ); + PearlAssociatedObjectProperty( NSMutableArray*, ProductObservers, productObservers ); - (void)registerProductsObserver:(id)delegate { @@ -101,11 +102,25 @@ PearlAssociatedObjectProperty( NSMutableArray*, ProductObservers, productObserve } #endif - for (SKProduct *product in self.products) + for (SKProduct *product in [self.products allValues]) if ([product.productIdentifier isEqualToString:productIdentifier]) { SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product]; - payment.quantity = quantity; - [[self paymentQueue] addPayment:payment]; + 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; } } @@ -114,8 +129,13 @@ PearlAssociatedObjectProperty( NSMutableArray*, ProductObservers, productObserve - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { - inf( @"products: %@, invalid: %@", response.products, response.invalidProductIdentifiers ); - self.products = response.products; + 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]; @@ -123,6 +143,8 @@ PearlAssociatedObjectProperty( NSMutableArray*, ProductObservers, productObserve - (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, @@ -131,7 +153,6 @@ PearlAssociatedObjectProperty( NSMutableArray*, ProductObservers, productObserve cancelTitle:@"OK" otherTitles:nil]; #else #endif - err( @"StoreKit request (%@) failed: %@", request, [error fullDescription] ); } - (void)requestDidFinish:(SKRequest *)request { @@ -147,21 +168,37 @@ PearlAssociatedObjectProperty( NSMutableArray*, ProductObservers, productObserve dbg( @"transaction updated: %@ -> %d", transaction.payment.productIdentifier, (int)(transaction.transactionState) ); switch (transaction.transactionState) { case SKPaymentTransactionStatePurchased: { - inf( @"purchased: %@", transaction.payment.productIdentifier ); + 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]; + [Answers logPurchaseWithPrice:product.price currency:product.priceLocale.currencyCode success:@YES + itemName:product.localizedTitle itemType:@"InApp" itemId:product.productIdentifier + customAttributes:attributes]; +#endif + } break; } case SKPaymentTransactionStateRestored: { - inf( @"restored: %@", transaction.payment.productIdentifier ); + inf( @"Restored: %@", transaction.payment.productIdentifier ); [[NSUserDefaults standardUserDefaults] setObject:transaction.transactionIdentifier forKey:transaction.payment.productIdentifier]; [queue finishTransaction:transaction]; @@ -173,6 +210,18 @@ PearlAssociatedObjectProperty( NSMutableArray*, ProductObservers, productObserve case SKPaymentTransactionStateFailed: err( @"Transaction failed: %@, reason: %@", transaction.payment.productIdentifier, [transaction.error fullDescription] ); [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 + customAttributes:@{ + @"state" : @"Failed", + @"reason": [transaction.error fullDescription], + }]; +#endif + } break; } @@ -185,7 +234,7 @@ PearlAssociatedObjectProperty( NSMutableArray*, ProductObservers, productObserve - (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error { - err( @"StoreKit restore failed: %@", [error fullDescription] ); + MPError( error, @"StoreKit restore failed." ); } @end diff --git a/platform-darwin/Source/MPAppDelegate_Key.m b/platform-darwin/Source/MPAppDelegate_Key.m index d025c63f..39b71785 100644 --- a/platform-darwin/Source/MPAppDelegate_Key.m +++ b/platform-darwin/Source/MPAppDelegate_Key.m @@ -16,12 +16,13 @@ // LICENSE file. Alternatively, see . //============================================================================== +#import #import "MPAppDelegate_Key.h" #import "MPAppDelegate_Store.h" @interface MPAppDelegate_Shared() -@property(strong, nonatomic) MPKey *key; +@property(strong, atomic) MPKey *key; @end @@ -177,6 +178,14 @@ static NSDictionary *createKeyQuery(MPUserEntity *user, BOOL newItem, MPKeyOrigi else dbg( @"Automatic login failed for user: %@", user.userID ); + if ([[MPConfig get].sendInfo boolValue]) { +#ifdef CRASHLYTICS + [Answers logLoginWithMethod:password? @"Password": @"Automatic" success:@NO customAttributes:@{ + @"algorithm": @(user.algorithm.version), + }]; +#endif + } + return NO; } inf( @"Logged in user: %@", user.userID ); @@ -198,8 +207,11 @@ static NSDictionary *createKeyQuery(MPUserEntity *user, BOOL newItem, MPKeyOrigi @try { if ([[MPConfig get].sendInfo boolValue]) { #ifdef CRASHLYTICS - [[Crashlytics sharedInstance] setObjectValue:user.userID forKey:@"username"]; [[Crashlytics sharedInstance] setUserName:user.userID]; + + [Answers logLoginWithMethod:password? @"Password": @"Automatic" success:@YES customAttributes:@{ + @"algorithm": @(user.algorithm.version), + }]; #endif } } diff --git a/platform-darwin/Source/MPAppDelegate_Shared.h b/platform-darwin/Source/MPAppDelegate_Shared.h index cc1e4ed4..d2b832b4 100644 --- a/platform-darwin/Source/MPAppDelegate_Shared.h +++ b/platform-darwin/Source/MPAppDelegate_Shared.h @@ -26,9 +26,9 @@ #endif -@property(strong, nonatomic, readonly) MPKey *key; -@property(strong, nonatomic, readonly) NSManagedObjectID *activeUserOID; -@property(strong, nonatomic, readonly) NSPersistentStoreCoordinator *storeCoordinator; +@property(strong, atomic, readonly) MPKey *key; +@property(strong, atomic, readonly) NSManagedObjectID *activeUserOID; +@property(strong, atomic, readonly) NSPersistentStoreCoordinator *storeCoordinator; + (instancetype)get; diff --git a/platform-darwin/Source/MPAppDelegate_Shared.m b/platform-darwin/Source/MPAppDelegate_Shared.m index af21f8c4..3b867874 100644 --- a/platform-darwin/Source/MPAppDelegate_Shared.m +++ b/platform-darwin/Source/MPAppDelegate_Shared.m @@ -23,9 +23,9 @@ @interface MPAppDelegate_Shared() -@property(strong, nonatomic) MPKey *key; -@property(strong, nonatomic) NSManagedObjectID *activeUserOID; -@property(strong, nonatomic) NSPersistentStoreCoordinator *storeCoordinator; +@property(strong, atomic) MPKey *key; +@property(strong, atomic) NSManagedObjectID *activeUserOID; +@property(strong, atomic) NSPersistentStoreCoordinator *storeCoordinator; @end @@ -76,12 +76,13 @@ NSError *error; if (activeUser.objectID.isTemporaryID && ![activeUser.managedObjectContext obtainPermanentIDsForObjects:@[ activeUser ] error:&error]) - err( @"Failed to obtain a permanent object ID after setting active user: %@", [error fullDescription] ); + MPError( error, @"Failed to obtain a permanent object ID after setting active user." ); self.activeUserOID = activeUser.objectID; } - (void)handleCoordinatorError:(NSError *)error { + } @end diff --git a/platform-darwin/Source/MPAppDelegate_Store.m b/platform-darwin/Source/MPAppDelegate_Store.m index 878374bd..66af701c 100644 --- a/platform-darwin/Source/MPAppDelegate_Store.m +++ b/platform-darwin/Source/MPAppDelegate_Store.m @@ -237,7 +237,7 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted ); NSURL *localStoreURL = [self localStoreURL]; if (![[NSFileManager defaultManager] createDirectoryAtURL:[localStoreURL URLByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:&error]) { - err( @"Couldn't create our application support directory: %@", [error fullDescription] ); + MPError( error, @"Couldn't create our application support directory." ); return; } if (![self.storeCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[self localStoreURL] @@ -246,7 +246,7 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted ); NSInferMappingModelAutomaticallyOption : @YES, STORE_OPTIONS } error:&error]) { - err( @"Failed to open store: %@", [error fullDescription] ); + MPError( error, @"Failed to open store." ); self.storeCorrupted = @YES; [self handleCoordinatorError:error]; return; @@ -255,12 +255,15 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted ); #if TARGET_OS_IPHONE PearlAddNotificationObserver( UIApplicationWillResignActiveNotification, UIApp, [NSOperationQueue mainQueue], + ^(MPAppDelegate_Shared *self, NSNotification *note) { + [self.mainManagedObjectContext saveToStore]; + } ); #else PearlAddNotificationObserver( NSApplicationWillResignActiveNotification, NSApp, [NSOperationQueue mainQueue], -#endif ^(MPAppDelegate_Shared *self, NSNotification *note) { - [self.mainManagedObjectContext saveToStore]; - } ); + [self.mainManagedObjectContext saveToStore]; + } ); +#endif // Perform a data sanity check on the newly loaded store to find and fix any issues. if ([[MPConfig get].checkInconsistency boolValue]) @@ -286,10 +289,10 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted ); NSError *error = nil; for (NSPersistentStore *store in self.storeCoordinator.persistentStores) { if (![self.storeCoordinator removePersistentStore:store error:&error]) - err( @"Couldn't remove persistence store from coordinator: %@", [error fullDescription] ); + MPError( error, @"Couldn't remove persistence store from coordinator." ); } if (![[NSFileManager defaultManager] removeItemAtURL:self.localStoreURL error:&error]) - err( @"Couldn't remove persistence store at URL %@: %@", self.localStoreURL, [error fullDescription] ); + MPError( error, @"Couldn't remove persistence store at URL %@.", self.localStoreURL ); [self loadStore]; } @@ -307,7 +310,7 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted ); fetchRequest.entity = entity; NSArray *objects = [context executeFetchRequest:fetchRequest error:&error]; if (!objects) { - err( @"Failed to fetch %@ objects: %@", entity, [error fullDescription] ); + MPError( error, @"Failed to fetch %@ objects.", entity ); continue; } @@ -332,7 +335,8 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted ); - (void)migrateStore { - MPStoreMigrationLevel migrationLevel = (signed)[[NSUserDefaults standardUserDefaults] integerForKey:MPMigrationLevelLocalStoreKey]; + MPStoreMigrationLevel migrationLevel = (MPStoreMigrationLevel) + [[NSUserDefaults standardUserDefaults] integerForKey:MPMigrationLevelLocalStoreKey]; if (migrationLevel >= MPStoreMigrationLevelCurrent) // Local store up-to-date. return; @@ -375,7 +379,7 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted ); if (![NSPersistentStore migrateStore:oldLocalStoreURL withOptions:@{ STORE_OPTIONS } toStore:newLocalStoreURL withOptions:@{ STORE_OPTIONS } model:nil error:&error]) { - err( @"Couldn't migrate the old store to the new location: %@", [error fullDescription] ); + MPError( error, @"Couldn't migrate the old store to the new location." ); return NO; } @@ -422,7 +426,7 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted ); NSInferMappingModelAutomaticallyOption : @YES, STORE_OPTIONS } model:nil error:&error]) { - err( @"Couldn't migrate the old store to the new location: %@", [error fullDescription] ); + MPError( error, @"Couldn't migrate the old store to the new location." ); return NO; } @@ -459,7 +463,7 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted ); NSError *error = nil; if (site.objectID.isTemporaryID && ![context obtainPermanentIDsForObjects:@[ site ] error:&error]) - err( @"Failed to obtain a permanent object ID after creating new site: %@", [error fullDescription] ); + MPError( error, @"Failed to obtain a permanent object ID after creating new site." ); [context saveToStore]; @@ -491,7 +495,7 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted ); NSError *error = nil; if (![context obtainPermanentIDsForObjects:@[ newSite ] error:&error]) - err( @"Failed to obtain a permanent object ID after changing object type: %@", [error fullDescription] ); + MPError( error, @"Failed to obtain a permanent object ID after changing object type." ); [context deleteObject:site]; [context saveToStore]; @@ -537,7 +541,7 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted ); initWithPattern:@"^#[[:space:]]*([^:]+): (.*)" options:(NSRegularExpressionOptions)0 error:&error]; if (error) { - err( @"Error loading the header pattern: %@", [error fullDescription] ); + MPError( error, @"Error loading the header pattern." ); return MPImportResultInternalError; } } @@ -551,7 +555,7 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted ); options:(NSRegularExpressionOptions)0 error:&error] ]; if (error) { - err( @"Error loading the site patterns: %@", [error fullDescription] ); + MPError( error, @"Error loading the site patterns." ); return MPImportResultInternalError; } } @@ -610,7 +614,7 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted ); userFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@", importUserName]; NSArray *users = [context executeFetchRequest:userFetchRequest error:&error]; if (!users) { - err( @"While looking for user: %@, error: %@", importUserName, [error fullDescription] ); + MPError( error, @"While looking for user: %@.", importUserName ); return MPImportResultInternalError; } if ([users count] > 1) { @@ -694,7 +698,7 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted ); siteFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND user == %@", siteName, user]; NSArray *existingSites = [context executeFetchRequest:siteFetchRequest error:&error]; if (!existingSites) { - err( @"Lookup of existing sites failed for site: %@, user: %@, error: %@", siteName, user.userID, [error fullDescription] ); + MPError( error, @"Lookup of existing sites failed for site: %@, user: %@.", siteName, user.userID ); return MPImportResultInternalError; } if ([existingSites count]) { diff --git a/platform-darwin/Source/MPConfig.m b/platform-darwin/Source/MPConfig.m index 1503dcc4..3c4b3e0b 100644 --- a/platform-darwin/Source/MPConfig.m +++ b/platform-darwin/Source/MPConfig.m @@ -20,7 +20,7 @@ [self.defaults registerDefaults:@{ NSStringFromSelector( @selector( askForReviews ) ) : @YES, - NSStringFromSelector( @selector( sendInfo ) ) : @NO, + NSStringFromSelector( @selector( sendInfo ) ) : @YES, NSStringFromSelector( @selector( rememberLogin ) ) : @NO, NSStringFromSelector( @selector( hidePasswords ) ) : @NO, NSStringFromSelector( @selector( checkInconsistency ) ): @NO, diff --git a/platform-darwin/Source/MPEntities.m b/platform-darwin/Source/MPEntities.m index f7157825..0c413529 100644 --- a/platform-darwin/Source/MPEntities.m +++ b/platform-darwin/Source/MPEntities.m @@ -21,11 +21,11 @@ @try { NSError *error = nil; if (!(success = [self save:&error])) - err( @"While saving: %@", [error fullDescription] ); + MPError( error, @"While saving." ); } @catch (NSException *exception) { success = NO; - err( @"While saving: %@", [exception fullDescription] ); + err( @"While saving.\n%@", [exception fullDescription] ); } }]; diff --git a/platform-darwin/Source/MPTypes.h b/platform-darwin/Source/MPTypes.h index 29bf750c..85237259 100644 --- a/platform-darwin/Source/MPTypes.h +++ b/platform-darwin/Source/MPTypes.h @@ -6,6 +6,9 @@ // Copyright (c) 2012 Lyndir. All rights reserved. // +#import +#import + __BEGIN_DECLS extern NSString *const MPErrorDomain; @@ -19,4 +22,21 @@ extern NSString *const MPFoundInconsistenciesNotification; extern NSString *const MPSitesImportedNotificationUserKey; extern NSString *const MPInconsistenciesFixResultUserKey; + __END_DECLS + +#ifdef CRASHLYTICS +#define MPError(error_, message, ...) ({ \ + err( message @"%@%@", ##__VA_ARGS__, error_? @"\n": @"", [error_ fullDescription]?: @"" ); \ + \ + if ([[MPConfig get].sendInfo boolValue]) { \ + [[Crashlytics sharedInstance] recordError:error_ withAdditionalUserInfo:@{ \ + @"location": strf( @"%@:%d %@", @(basename((char *)__FILE__)), __LINE__, NSStringFromSelector(_cmd) ), \ + }]; \ + } \ +}) +#else +#define MPError(error_, ...) ({ \ + err( message @"%@%@", ##__VA_ARGS__, error_? @"\n": @"", [error_ fullDescription]?: @"" ); \ +}) +#endif diff --git a/platform-darwin/Source/Mac/MPMacAppDelegate.m b/platform-darwin/Source/Mac/MPMacAppDelegate.m index f6aaa934..60680018 100644 --- a/platform-darwin/Source/Mac/MPMacAppDelegate.m +++ b/platform-darwin/Source/Mac/MPMacAppDelegate.m @@ -78,7 +78,6 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven [[Crashlytics sharedInstance] setUserIdentifier:[PearlKeyChain deviceIdentifier]]; [[Crashlytics sharedInstance] setObjectValue:[PearlKeyChain deviceIdentifier] forKey:@"deviceIdentifier"]; [[Crashlytics sharedInstance] setUserName:@"Anonymous"]; - [[Crashlytics sharedInstance] setObjectValue:@"Anonymous" forKey:@"username"]; [Crashlytics startWithAPIKey:crashlyticsAPIKey]; [[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) { PearlLogLevel level = PearlLogLevelInfo; @@ -272,7 +271,7 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven [[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler: ^(NSData *importedSitesData, NSURLResponse *response, NSError *error) { if (error) - err( @"While reading imported sites from %@: %@", url, [error fullDescription] ); + MPError( error, @"While reading imported sites from %@.", url ); if (!importedSitesData) return; @@ -394,9 +393,9 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven inManagedObjectContext:moc]; newUser.name = name; [moc saveToStore]; - NSError *error = nil; - if (![moc obtainPermanentIDsForObjects:@[ newUser ] error:&error]) - err( @"Failed to obtain permanent object ID for new user: %@", [error fullDescription] ); +// NSError *error = nil; +// if (![moc obtainPermanentIDsForObjects:@[ newUser ] error:&error]) +// MPError( error, @"Failed to obtain permanent object ID for new user." ); [[NSOperationQueue mainQueue] addOperationWithBlock:^{ [self updateUsers]; @@ -516,20 +515,23 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven NSError *coordinateError = nil; NSString *exportedSites = [self exportSitesRevealPasswords:revealPasswords]; - [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateWritingItemAtURL:savePanel.URL options:0 error:&coordinateError - byAccessor:^(NSURL *newURL) { - NSError *writeError = nil; - if (![exportedSites writeToURL:newURL atomically:NO - encoding:NSUTF8StringEncoding - error:&writeError]) - PearlMainQueue( ^{ - [[NSAlert alertWithError:writeError] runModal]; - } ); - }]; - if (coordinateError) + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateWritingItemAtURL:savePanel.URL options:0 + error:&coordinateError byAccessor: + ^(NSURL *newURL) { + NSError *writeError = nil; + if (![exportedSites writeToURL:newURL atomically:NO encoding:NSUTF8StringEncoding error:&writeError]) + MPError( writeError, @"Could not write to the export file." ); + + PearlMainQueue( ^{ + [[NSAlert alertWithError:writeError] runModal]; + } ); + }]; + if (coordinateError) { + MPError( coordinateError, @"Write access to the export file could not be obtained." ); PearlMainQueue( ^{ [[NSAlert alertWithError:coordinateError] runModal]; } ); + } } - (void)updateUsers { @@ -567,7 +569,7 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven fetchRequest.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"lastUsed" ascending:NO] ]; NSArray *users = [mainContext executeFetchRequest:fetchRequest error:&error]; if (!users) - err( @"Failed to load users: %@", [error fullDescription] ); + MPError( error, @"Failed to load users." ); if (![users count]) { NSMenuItem *noUsersItem = [self.usersItem.submenu addItemWithTitle:@"No users" action:NULL keyEquivalent:@""]; diff --git a/platform-darwin/Source/Mac/MPPasswordWindowController.m b/platform-darwin/Source/Mac/MPPasswordWindowController.m index 4f0af45b..50963200 100644 --- a/platform-darwin/Source/Mac/MPPasswordWindowController.m +++ b/platform-darwin/Source/Mac/MPPasswordWindowController.m @@ -555,7 +555,7 @@ NSArray *siteResults = [context executeFetchRequest:fetchRequest error:&error]; if (!siteResults) { prof_finish( @"executeFetchRequest: %@ // %@", fetchRequest.predicate, [error fullDescription] ); - err( @"While fetching sites for completion: %@", [error fullDescription] ); + MPError( error, @"While fetching sites for completion." ); return; } prof_rewind( @"executeFetchRequest: %@", fetchRequest.predicate ); diff --git a/platform-darwin/Source/Mac/MPSiteModel.m b/platform-darwin/Source/Mac/MPSiteModel.m index 9673757a..be650560 100644 --- a/platform-darwin/Source/Mac/MPSiteModel.m +++ b/platform-darwin/Source/Mac/MPSiteModel.m @@ -115,7 +115,7 @@ NSError *error; MPSiteEntity *entity = (MPSiteEntity *)[moc existingObjectWithID:_entityOID error:&error]; if (!entity) - err( @"Couldn't retrieve active site: %@", [error fullDescription] ); + MPError( error, @"Couldn't retrieve active site." ); return entity; } diff --git a/platform-darwin/Source/iOS/MPAnswersViewController.m b/platform-darwin/Source/iOS/MPAnswersViewController.m index 14f2f0a0..f5f7df6f 100644 --- a/platform-darwin/Source/iOS/MPAnswersViewController.m +++ b/platform-darwin/Source/iOS/MPAnswersViewController.m @@ -333,12 +333,12 @@ question.keyword = keyword; if ([context saveToStore]) { - if ([question.objectID isTemporaryID]) { - NSError *error = nil; - [context obtainPermanentIDsForObjects:@[ question ] error:&error]; - if (error) - err( @"Failed to obtain permanent object ID: %@", [error fullDescription] ); - } +// if ([question.objectID isTemporaryID]) { +// NSError *error = nil; +// [context obtainPermanentIDsForObjects:@[ question ] error:&error]; +// if (error) +// MPError( error, @"Failed to obtain permanent object ID: %@" ); +// } _questionOID = question.objectID; [self updateAnswerForQuestion:question ofSite:site]; diff --git a/platform-darwin/Source/iOS/MPPasswordsViewController.m b/platform-darwin/Source/iOS/MPPasswordsViewController.m index 82e42d6d..b90d8fed 100644 --- a/platform-darwin/Source/iOS/MPPasswordsViewController.m +++ b/platform-darwin/Source/iOS/MPPasswordsViewController.m @@ -395,7 +395,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { self.fetchedResultsController.fetchRequest.predicate = [NSPredicate predicateWithFormat:@"name LIKE[cd] %@ AND user == %@", queryPattern, [MPiOSAppDelegate get].activeUserOID]; if (![self.fetchedResultsController performFetch:&error]) - err( @"Couldn't fetch sites: %@", [error fullDescription] ); + MPError( error, @"Couldn't fetch sites." ); PearlMainQueue( ^{ [self.passwordCollectionView updateDataSource:_passwordCollectionSections diff --git a/platform-darwin/Source/iOS/MPStoreViewController.h b/platform-darwin/Source/iOS/MPStoreViewController.h index 1cada686..e4ce015f 100644 --- a/platform-darwin/Source/iOS/MPStoreViewController.h +++ b/platform-darwin/Source/iOS/MPStoreViewController.h @@ -39,6 +39,8 @@ @interface MPStoreProductCell : UITableViewCell @property(nonatomic) IBOutlet UILabel *priceLabel; +@property(nonatomic) IBOutlet UILabel *titleLabel; +@property(nonatomic) IBOutlet UILabel *descriptionLabel; @property(nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator; @property(nonatomic) IBOutlet UIView *purchasedIndicator; diff --git a/platform-darwin/Source/iOS/MPStoreViewController.m b/platform-darwin/Source/iOS/MPStoreViewController.m index 17021827..64f28a84 100644 --- a/platform-darwin/Source/iOS/MPStoreViewController.m +++ b/platform-darwin/Source/iOS/MPStoreViewController.m @@ -28,7 +28,7 @@ PearlEnum( MPDevelopmentFuelConsumption, @interface MPStoreViewController() @property(nonatomic, strong) NSNumberFormatter *currencyFormatter; -@property(nonatomic, strong) NSArray *products; +@property(nonatomic, strong) NSDictionary *products; @end @@ -43,7 +43,7 @@ PearlEnum( MPDevelopmentFuelConsumption, ]; NSInteger storeVersion = [[NSUserDefaults standardUserDefaults] integerForKey:@"storeVersion"]; for (; storeVersion < [storeVersions count]; ++storeVersion) - [features appendFormat:@"%@\n", storeVersions[storeVersion]]; + [features appendFormat:@"%@\n", storeVersions[(NSUInteger)storeVersion]]; if (![features length]) return nil; @@ -170,7 +170,7 @@ PearlEnum( MPDevelopmentFuelConsumption, #pragma mark - MPInAppDelegate -- (void)updateWithProducts:(NSArray *)products { +- (void)updateWithProducts:(NSDictionary *)products { self.products = products; @@ -218,7 +218,7 @@ PearlEnum( MPDevelopmentFuelConsumption, - (SKProduct *)productForCell:(MPStoreProductCell *)cell { - for (SKProduct *product in self.products) + for (SKProduct *product in [self.products allValues]) if ([self cellForProductIdentifier:product.productIdentifier] == cell) return product; @@ -248,7 +248,7 @@ PearlEnum( MPDevelopmentFuelConsumption, [hideCells addObjectsFromArray:[self.allCellsBySection[0] array]]; [hideCells addObject:self.loadingCell]; - for (SKProduct *product in self.products) { + 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]; @@ -313,6 +313,8 @@ PearlEnum( MPDevelopmentFuelConsumption, 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; } diff --git a/platform-darwin/Source/iOS/MPUsersViewController.m b/platform-darwin/Source/iOS/MPUsersViewController.m index 52cf53b4..869a20ca 100644 --- a/platform-darwin/Source/iOS/MPUsersViewController.m +++ b/platform-darwin/Source/iOS/MPUsersViewController.m @@ -16,6 +16,7 @@ // LICENSE file. Alternatively, see . //============================================================================== +#import #import "MPUsersViewController.h" #import "MPEntities.h" #import "MPAvatarCell.h" @@ -224,6 +225,17 @@ typedef NS_ENUM( NSUInteger, MPActiveUserState ) { user.defaultType = user.algorithm.defaultType; user.avatar = newUserAvatar; user.name = newUserName; + + if ([[MPConfig get].sendInfo boolValue]) { +#ifdef CRASHLYTICS + [Answers logSignUpWithMethod:@"Manual" + success:@YES + customAttributes:@{ + @"algorithm": @(user.algorithm.version), + @"avatar" : @(user.avatar), + }]; +#endif + } } BOOL signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context @@ -719,7 +731,7 @@ referenceSizeForFooterInSection:(NSInteger)section { ]; NSArray *users = [mainContext executeFetchRequest:fetchRequest error:&error]; if (!users) { - err( @"Failed to load users: %@", [error fullDescription] ); + MPError( error, @"Failed to load users." ); self.userIDs = nil; } diff --git a/platform-darwin/Source/iOS/MPiOSAppDelegate.m b/platform-darwin/Source/iOS/MPiOSAppDelegate.m index 02375c5f..67292953 100644 --- a/platform-darwin/Source/iOS/MPiOSAppDelegate.m +++ b/platform-darwin/Source/iOS/MPiOSAppDelegate.m @@ -25,7 +25,6 @@ @property(nonatomic, strong) UIDocumentInteractionController *interactionController; -@property(nonatomic) UIBackgroundTaskIdentifier task; @end @implementation MPiOSAppDelegate @@ -59,10 +58,9 @@ [[Crashlytics sharedInstance] setUserIdentifier:[PearlKeyChain deviceIdentifier]]; [[Crashlytics sharedInstance] setObjectValue:[PearlKeyChain deviceIdentifier] forKey:@"deviceIdentifier"]; [[Crashlytics sharedInstance] setUserName:@"Anonymous"]; - [[Crashlytics sharedInstance] setObjectValue:@"Anonymous" forKey:@"username"]; [Crashlytics startWithAPIKey:crashlyticsAPIKey]; [[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) { - PearlLogLevel level = PearlLogLevelInfo; + PearlLogLevel level = PearlLogLevelWarn; if ([[MPConfig get].sendInfo boolValue]) level = PearlLogLevelDebug; @@ -83,9 +81,6 @@ PearlAddNotificationObserver( MPCheckConfigNotification, nil, [NSOperationQueue mainQueue], ^(id self, NSNotification *note) { [self updateConfigKey:note.object]; } ); -// PearlAddNotificationObserver( kIASKAppSettingChanged, nil, nil, ^(id self, NSNotification *note) { -// [[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:note.object]; -// } ); PearlAddNotificationObserver( NSUserDefaultsDidChangeNotification, nil, nil, ^(id self, NSNotification *note) { [[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:nil]; } ); @@ -155,7 +150,8 @@ [[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler: ^(NSData *importedSitesData, NSURLResponse *response, NSError *error) { if (error) - err( @"While reading imported sites from %@: %@", url, [error fullDescription] ); + MPError( error, @"While reading imported sites from %@.", url ); + if (!importedSitesData) { [PearlAlert showError:strf( @"Master Password couldn't read the import sites.\n\n%@", [error localizedDescription]?: error )]; @@ -483,7 +479,7 @@ NSError *error = nil; if (![[exportedSites dataUsingEncoding:NSUTF8StringEncoding] writeToURL:exportURL options:NSDataWritingFileProtectionComplete error:&error]) - err( @"Failed to write export data to URL %@: %@", exportURL, [error fullDescription] ); + MPError( error, @"Failed to write export data to URL %@.", exportURL ); else { self.interactionController = [UIDocumentInteractionController interactionControllerWithURL:exportURL]; self.interactionController.UTI = @"com.lyndir.masterpassword.sites"; @@ -574,7 +570,7 @@ static NSDictionary *crashlyticsInfo = nil; if (crashlyticsInfo == nil) crashlyticsInfo = [[NSDictionary alloc] initWithContentsOfURL: - [[NSBundle mainBundle] URLForResource:@"Crashlytics" withExtension:@"plist"]]; + [[NSBundle mainBundle] URLForResource:@"Fabric" withExtension:@"plist"]]; return crashlyticsInfo; } diff --git a/platform-darwin/Source/iOS/MasterPassword-Info.plist b/platform-darwin/Source/iOS/MasterPassword-Info.plist index 2d8b08c5..de00a7aa 100644 --- a/platform-darwin/Source/iOS/MasterPassword-Info.plist +++ b/platform-darwin/Source/iOS/MasterPassword-Info.plist @@ -60,7 +60,7 @@ LSRequiresIPhoneOS NSHumanReadableCopyright - © 2011-2016 Lyndir + © 2011-2017 UIAppFonts Exo2.0-Bold.otf diff --git a/platform-darwin/Source/iOS/Settings.bundle/Root.plist b/platform-darwin/Source/iOS/Settings.bundle/Root.plist index 981c5279..6b161689 100644 --- a/platform-darwin/Source/iOS/Settings.bundle/Root.plist +++ b/platform-darwin/Source/iOS/Settings.bundle/Root.plist @@ -6,7 +6,7 @@ FooterText - Enable this setting to send us carefully anonymized information to help us diagnose and resolve issues you might experience in the future. + Enable this setting to send carefully anonymized information to help us diagnose and resolve any issues you encounter in a future update. Title Type @@ -50,7 +50,7 @@ Key sendInfo DefaultValue - + FooterText diff --git a/platform-darwin/Source/iOS/Storyboard.storyboard b/platform-darwin/Source/iOS/Storyboard.storyboard index f19db4c9..77a69134 100644 --- a/platform-darwin/Source/iOS/Storyboard.storyboard +++ b/platform-darwin/Source/iOS/Storyboard.storyboard @@ -2620,8 +2620,10 @@ See + + @@ -2685,8 +2687,10 @@ See + + @@ -2750,8 +2754,10 @@ See + + @@ -2815,8 +2821,10 @@ See + + @@ -2908,7 +2916,9 @@ invested: 3.7 work hours + +