From 9662b3cb1e7b473a8582bb23c1ac04365c717f47 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Tue, 7 Aug 2018 18:55:24 -0600 Subject: [PATCH] Wait a week before nagging when a new release comes out --- Podfile | 1 - Podfile.lock | 6 +- Pods | 2 +- Signal.xcodeproj/project.pbxproj | 12 +- Signal/src/AppDelegate.m | 1 - Signal/src/Signal-Bridging-Header.h | 1 + Signal/src/util/AppUpdateNag.h | 13 -- Signal/src/util/AppUpdateNag.m | 118 -------------- Signal/src/util/AppUpdateNag.swift | 233 ++++++++++++++++++++++++++++ 9 files changed, 240 insertions(+), 147 deletions(-) delete mode 100644 Signal/src/util/AppUpdateNag.h delete mode 100644 Signal/src/util/AppUpdateNag.m create mode 100644 Signal/src/util/AppUpdateNag.swift diff --git a/Podfile b/Podfile index b719f1437..a161c4805 100644 --- a/Podfile +++ b/Podfile @@ -32,7 +32,6 @@ end target 'Signal' do shared_pods - pod 'ATAppUpdater', :inhibit_warnings => true pod 'SSZipArchive', :inhibit_warnings => true target 'SignalTests' do diff --git a/Podfile.lock b/Podfile.lock index 12a0d8224..3bb4b98fa 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -14,7 +14,6 @@ PODS: - AFNetworking/Serialization (3.2.1) - AFNetworking/UIKit (3.2.1): - AFNetworking/NSURLSession - - ATAppUpdater (2.0) - AxolotlKit (0.9.0): - CocoaLumberjack - Curve25519Kit (~> 2.1.0) @@ -137,7 +136,6 @@ PODS: DEPENDENCIES: - AFNetworking - - ATAppUpdater - AxolotlKit (from `https://github.com/signalapp/SignalProtocolKit.git`) - Curve25519Kit (from `https://github.com/signalapp/Curve25519Kit`) - GRKOpenSSLFramework (from `https://github.com/signalapp/GRKOpenSSLFramework`) @@ -155,7 +153,6 @@ DEPENDENCIES: SPEC REPOS: https://github.com/cocoapods/specs.git: - AFNetworking - - ATAppUpdater - CocoaLumberjack - libPhoneNumber-iOS - Mantle @@ -213,7 +210,6 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: AFNetworking: b6f891fdfaed196b46c7a83cf209e09697b94057 - ATAppUpdater: a9f7027060959d47e58733d3b48f6b9a28cb8de1 AxolotlKit: 07bd978ea931d113939de88d3d2d354896680ceb CocoaLumberjack: db7cc9e464771f12054c22ff6947c5a58d43a0fd Curve25519Kit: 76d0859ecb34704f7732847812363f83b23a6a59 @@ -233,6 +229,6 @@ SPEC CHECKSUMS: YapDatabase: b418a4baa6906e8028748938f9159807fd039af4 YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54 -PODFILE CHECKSUM: 0c3d5978b54712391b5b90835b9554277b07fa23 +PODFILE CHECKSUM: cb4f38eaa6b1bdf86cfe440ef964c628e6d8321d COCOAPODS: 1.5.3 diff --git a/Pods b/Pods index 5da11dee0..73bf1779e 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit 5da11dee08c2cc8864ea03b1489bd8898d28dd4e +Subproject commit 73bf1779e0298cbc28c0b93d908bae0aa1f44bbc diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 5f17093e8..fe3934ec4 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -209,7 +209,6 @@ 34C82E5120F8E1F300E9688D /* Theme.h in Headers */ = {isa = PBXBuildFile; fileRef = 34C82E4F20F8E1F000E9688D /* Theme.h */; settings = {ATTRIBUTES = (Public, ); }; }; 34C82E5220F8E1F300E9688D /* Theme.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C82E5020F8E1F100E9688D /* Theme.m */; }; 34CA631B2097806F00E526A0 /* OWSContactShareView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CA631A2097806E00E526A0 /* OWSContactShareView.m */; }; - 34CCAF381F0C0599004084F4 /* AppUpdateNag.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CCAF371F0C0599004084F4 /* AppUpdateNag.m */; }; 34CE88E71F2FB9A10098030F /* ProfileViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CE88E61F2FB9A10098030F /* ProfileViewController.m */; }; 34CF0787203E6B78005C4D61 /* busy_tone_ansi.caf in Resources */ = {isa = PBXBuildFile; fileRef = 34CF0783203E6B77005C4D61 /* busy_tone_ansi.caf */; }; 34CF0788203E6B78005C4D61 /* ringback_tone_ansi.caf in Resources */ = {isa = PBXBuildFile; fileRef = 34CF0784203E6B77005C4D61 /* ringback_tone_ansi.caf */; }; @@ -440,6 +439,7 @@ 4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB5F26820F7D060004D1B42 /* MessageActions.swift */; }; 4CC0B59C20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC0B59B20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift */; }; 4CC1ECF9211A47CE00CC13BE /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 4CC1ECFB211A553000CC13BE /* AppUpdateNag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */; }; 70377AAB1918450100CAF501 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70377AAA1918450100CAF501 /* MobileCoreServices.framework */; }; 768A1A2B17FC9CD300E00ED8 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 768A1A2A17FC9CD300E00ED8 /* libz.dylib */; }; 76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; }; @@ -868,8 +868,6 @@ 34CA1C281F7164F700E51C51 /* MediaMessageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaMessageView.swift; sourceTree = ""; }; 34CA63192097806E00E526A0 /* OWSContactShareView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactShareView.h; sourceTree = ""; }; 34CA631A2097806E00E526A0 /* OWSContactShareView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactShareView.m; sourceTree = ""; }; - 34CCAF361F0C0599004084F4 /* AppUpdateNag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppUpdateNag.h; sourceTree = ""; }; - 34CCAF371F0C0599004084F4 /* AppUpdateNag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppUpdateNag.m; sourceTree = ""; }; 34CE88E51F2FB9A10098030F /* ProfileViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ProfileViewController.h; sourceTree = ""; }; 34CE88E61F2FB9A10098030F /* ProfileViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ProfileViewController.m; sourceTree = ""; }; 34CF0783203E6B77005C4D61 /* busy_tone_ansi.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = busy_tone_ansi.caf; path = Signal/AudioFiles/busy_tone_ansi.caf; sourceTree = SOURCE_ROOT; }; @@ -1118,6 +1116,7 @@ 4CB5F26820F7D060004D1B42 /* MessageActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActions.swift; sourceTree = ""; }; 4CC0B59B20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationConfigurationSyncOperation.swift; sourceTree = ""; }; 4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + 4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateNag.swift; sourceTree = ""; }; 4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuActionsViewController.swift; sourceTree = ""; }; 69349DE607F5BA6036C9AC60 /* Pods-SignalShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.debug.xcconfig"; sourceTree = ""; }; 70377AAA1918450100CAF501 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; @@ -2174,8 +2173,6 @@ 76EB04C818170B33006006FC /* util */ = { isa = PBXGroup; children = ( - 34CCAF361F0C0599004084F4 /* AppUpdateNag.h */, - 34CCAF371F0C0599004084F4 /* AppUpdateNag.m */, B90418E4183E9DD40038554A /* DateUtil.h */, B90418E5183E9DD40038554A /* DateUtil.m */, 76EB04EA18170B33006006FC /* FunctionalUtil.h */, @@ -2206,6 +2203,7 @@ 34E5DC8120D8050D00C08145 /* RegistrationUtils.m */, 4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */, FCFA64B11A24F29E0007FB87 /* UI Categories */, + 4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */, ); path = util; sourceTree = ""; @@ -2991,7 +2989,6 @@ inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-Signal/Pods-Signal-frameworks.sh", "${BUILT_PRODUCTS_DIR}/AFNetworking/AFNetworking.framework", - "${BUILT_PRODUCTS_DIR}/ATAppUpdater/ATAppUpdater.framework", "${BUILT_PRODUCTS_DIR}/AxolotlKit/AxolotlKit.framework", "${BUILT_PRODUCTS_DIR}/CocoaLumberjack/CocoaLumberjack.framework", "${BUILT_PRODUCTS_DIR}/Curve25519Kit/Curve25519Kit.framework", @@ -3014,7 +3011,6 @@ name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AFNetworking.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ATAppUpdater.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AxolotlKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaLumberjack.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Curve25519Kit.framework", @@ -3288,7 +3284,6 @@ 34386A51207D0C01009F5D9C /* HomeViewController.m in Sources */, 34D1F0A91F867BFC0066283D /* ConversationViewCell.m in Sources */, 4505C2BF1E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */, - 34CCAF381F0C0599004084F4 /* AppUpdateNag.m in Sources */, EF764C351DB67CC5000D9A87 /* UIViewController+Permissions.m in Sources */, 45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */, 34D2CCE0206939B400CB1A14 /* DebugUIMessagesAssetLoader.m in Sources */, @@ -3356,6 +3351,7 @@ 34B3F8801E8DF1700035BE1A /* InviteFlow.swift in Sources */, 457C87B82032645C008D52D6 /* DebugUINotifications.swift in Sources */, 4C13C9F620E57BA30089A98B /* ColorPickerViewController.swift in Sources */, + 4CC1ECFB211A553000CC13BE /* AppUpdateNag.swift in Sources */, 340FC8D0205BF2FA007AEB0F /* OWSBackupIO.m in Sources */, 458E38371D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m in Sources */, 4517642B1DE939FD00EDB8B9 /* ContactCell.swift in Sources */, diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 687b0aa87..07c2d018f 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -3,7 +3,6 @@ // #import "AppDelegate.h" -#import "AppUpdateNag.h" #import "CodeVerificationViewController.h" #import "DebugLogger.h" #import "HomeViewController.h" diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index d496561eb..39527ac50 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -38,6 +38,7 @@ #import "PrivacySettingsTableViewController.h" #import "ProfileViewController.h" #import "PushManager.h" +#import "RegistrationViewController.h" #import "RemoteVideoView.h" #import "SignalApp.h" #import "UIViewController+Permissions.h" diff --git a/Signal/src/util/AppUpdateNag.h b/Signal/src/util/AppUpdateNag.h deleted file mode 100644 index e1f9eb186..000000000 --- a/Signal/src/util/AppUpdateNag.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -@interface AppUpdateNag : NSObject - -- (instancetype)init NS_UNAVAILABLE; - -+ (instancetype)sharedInstance; - -- (void)showAppUpgradeNagIfNecessary; - -@end diff --git a/Signal/src/util/AppUpdateNag.m b/Signal/src/util/AppUpdateNag.m deleted file mode 100644 index 9f776c761..000000000 --- a/Signal/src/util/AppUpdateNag.m +++ /dev/null @@ -1,118 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "AppUpdateNag.h" -#import "RegistrationViewController.h" -#import "Signal-Swift.h" -#import -#import -#import - -NSString *const OWSPrimaryStorageAppUpgradeNagCollection = @"TSStorageManagerAppUpgradeNagCollection"; -NSString *const OWSPrimaryStorageAppUpgradeNagDate = @"TSStorageManagerAppUpgradeNagDate"; - -@interface AppUpdateNag () - -@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; - -@end - -#pragma mark - - -@implementation AppUpdateNag - -+ (instancetype)sharedInstance -{ - static AppUpdateNag *sharedInstance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sharedInstance = [[self alloc] initDefault]; - }); - return sharedInstance; -} - -- (instancetype)initDefault -{ - OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager]; - - return [self initWithPrimaryStorage:primaryStorage]; -} - -- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage -{ - self = [super init]; - - if (!self) { - return self; - } - - OWSAssert(primaryStorage); - - _dbConnection = primaryStorage.newDatabaseConnection; - - OWSSingletonAssert(); - - return self; -} - -- (void)showAppUpgradeNagIfNecessary -{ - if (CurrentAppContext().isRunningTests) { - return; - } - - // Only show nag if we are "at rest" in the home view or registration view without any - // alerts or dialogs showing. - UIViewController *frontmostViewController = - [UIApplication sharedApplication].frontmostViewController; - OWSAssert(frontmostViewController); - BOOL canPresent = ([frontmostViewController isKindOfClass:[HomeViewController class]] || - [frontmostViewController isKindOfClass:[RegistrationViewController class]]); - if (!canPresent) { - return; - } - - NSDate *lastNagDate = [self.dbConnection dateForKey:OWSPrimaryStorageAppUpgradeNagDate - inCollection:OWSPrimaryStorageAppUpgradeNagCollection]; - const NSTimeInterval kNagFrequency = kDayInterval * 14; - BOOL canNag = (!lastNagDate || fabs(lastNagDate.timeIntervalSinceNow) > kNagFrequency); - if (!canNag) { - return; - } - - ATAppUpdater *updater = [ATAppUpdater sharedUpdater]; - [updater setAlertTitle:NSLocalizedString( - @"APP_UPDATE_NAG_ALERT_TITLE", @"Title for the 'new app version available' alert.")]; - [updater setAlertMessage:NSLocalizedString(@"APP_UPDATE_NAG_ALERT_MESSAGE_FORMAT", - @"Message format for the 'new app version available' alert. Embeds: {{The latest app " - @"version number.}}.")]; - [updater setAlertUpdateButtonTitle:NSLocalizedString(@"APP_UPDATE_NAG_ALERT_UPDATE_BUTTON", - @"Label for the 'update' button in the 'new app version available' alert.")]; - [updater setAlertCancelButtonTitle:CommonStrings.cancelButton]; - [updater setDelegate:self]; - [updater showUpdateWithConfirmation]; -} - -#pragma mark - ATAppUpdaterDelegate - -- (void)appUpdaterDidShowUpdateDialog -{ - DDLogInfo(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); - - [self.dbConnection setDate:[NSDate new] - forKey:OWSPrimaryStorageAppUpgradeNagDate - inCollection:OWSPrimaryStorageAppUpgradeNagCollection]; -} - -- (void)appUpdaterUserDidLaunchAppStore -{ - DDLogInfo(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); -} - -- (void)appUpdaterUserDidCancel -{ - DDLogInfo(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); -} - -@end diff --git a/Signal/src/util/AppUpdateNag.swift b/Signal/src/util/AppUpdateNag.swift new file mode 100644 index 000000000..416bacf8f --- /dev/null +++ b/Signal/src/util/AppUpdateNag.swift @@ -0,0 +1,233 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import PromiseKit + +@objc +class AppUpdateNag: NSObject { + + // MARK: Public + + @objc(sharedInstance) + public static let shared: AppUpdateNag = { + let versionService = AppStoreVersionService() + let nagManager = AppUpdateNag(versionService: versionService) + return nagManager + }() + + @objc + public func showAppUpgradeNagIfNecessary() { + + guard let currentVersion = self.currentVersion else { + owsFail("\(self.logTag) in \(#function) currentVersion was unexpectedly nil") + return + } + + guard let bundleIdentifier = self.bundleIdentifier else { + owsFail("\(self.logTag) in \(#function) bundleIdentifier was unexpectedly nil") + return + } + + guard let lookupURL = lookupURL(bundleIdentifier: bundleIdentifier) else { + owsFail("\(self.logTag) in \(#function) appStoreURL was unexpectedly nil") + return + } + + firstly { + self.versionService.fetchLatestVersion(lookupURL: lookupURL) + }.then { appStoreRecord -> Void in + guard appStoreRecord.version.compare(currentVersion, options: .numeric) == ComparisonResult.orderedDescending else { + Logger.debug("\(self.logTag) same old version: \(appStoreRecord)") + return + } + + Logger.info("\(self.logTag) new version available: \(appStoreRecord)") + self.showUpdateNagIfEnoughTimeHasPassed(appStoreRecord: appStoreRecord) + }.catch { error in + Logger.error("\(self.logTag) in \(#function) failed with error: \(error)") + }.retainUntilComplete() + } + + // MARK: - Internal + + let kUpgradeNagCollection = "TSStorageManagerAppUpgradeNagCollection" + let kLastNagDateKey = "TSStorageManagerAppUpgradeNagDate" + let kFirstHeardOfNewVersionDateKey = "TSStorageManagerAppUpgradeFirstHeardOfNewVersionDate" + + var dbConnection: YapDatabaseConnection { + return OWSPrimaryStorage.shared().dbReadWriteConnection + } + + // MARK: Bundle accessors + + var bundle: Bundle { + return Bundle.main + } + + var currentVersion: String? { + return bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + } + + var bundleIdentifier: String? { + return bundle.bundleIdentifier + } + + func lookupURL(bundleIdentifier: String) -> URL? { + return URL(string: "https://itunes.apple.com/lookup?bundleId=\(bundleIdentifier)") + } + + let versionService: AppStoreVersionService + + required init(versionService: AppStoreVersionService) { + self.versionService = versionService + super.init() + + SwiftSingletons.register(self) + } + + func showUpdateNagIfEnoughTimeHasPassed(appStoreRecord: AppStoreRecord) { + guard let firstHeardOfNewVersionDate = self.firstHeardOfNewVersionDate else { + self.setFirstHeardOfNewVersionDate(Date()) + return + } + + let intervalBeforeNag = 7 * kDayInterval + guard Date() > Date.init(timeInterval: intervalBeforeNag, since: firstHeardOfNewVersionDate) else { + Logger.info("\(logTag) in \(#function) firstHeardOfNewVersionDate: \(firstHeardOfNewVersionDate) not nagging for new release yet.") + return + } + + if let lastNagDate = self.lastNagDate { + let intervalBetweenNags = 14 * kDayInterval + guard Date() > Date.init(timeInterval: intervalBetweenNags, since: lastNagDate) else { + Logger.info("\(logTag) in \(#function) lastNagDate: \(lastNagDate) not nagging again so soon.") + return + } + } + + // Only show nag if we are "at rest" in the home view or registration view without any + // alerts or dialogs showing. + guard let frontmostViewController = UIApplication.shared.frontmostViewController else { + owsFail("\(self.logTag) in \(#function) frontmostViewController was unexpectedly nil") + return + } + + switch frontmostViewController { + case is HomeViewController, is RegistrationViewController: + self.setLastNagDate(Date()) + self.clearFirstHeardOfNewVersionDate() + presentUpgradeNag(appStoreRecord: appStoreRecord) + default: + Logger.debug("\(logTag) in \(#function) not presenting alert due to frontmostViewController: \(frontmostViewController)") + break + } + } + + func presentUpgradeNag(appStoreRecord: AppStoreRecord) { + let title = NSLocalizedString("APP_UPDATE_NAG_ALERT_TITLE", comment: "Title for the 'new app version available' alert.") + + let messageFormat = NSLocalizedString("APP_UPDATE_NAG_ALERT_MESSAGE_FORMAT", comment: "Message format for the 'new app version available' alert. Embeds: {{The latest app version number}}") + let message = String(format: messageFormat, appStoreRecord.version) + let buttonTitle = NSLocalizedString("APP_UPDATE_NAG_ALERT_UPDATE_BUTTON", comment: "Label for the 'update' button in the 'new app version available' alert.") + + OWSAlerts.showAlert(title: title, + message: message, + buttonTitle: buttonTitle, + buttonAction: { [weak self] _ in + guard let strongSelf = self else { + return + } + + strongSelf.showAppStore(appStoreURL: appStoreRecord.appStoreURL) + }) + } + + func showAppStore(appStoreURL: URL) { + Logger.debug("\(logTag) in \(#function)") + UIApplication.shared.openURL(appStoreURL) + } + + // MARK: Storage + + var firstHeardOfNewVersionDate: Date? { + return self.dbConnection.date(forKey: kFirstHeardOfNewVersionDateKey, inCollection: kUpgradeNagCollection) + } + + func setFirstHeardOfNewVersionDate(_ date: Date) { + self.dbConnection.setDate(date, forKey: kFirstHeardOfNewVersionDateKey, inCollection: kUpgradeNagCollection) + } + + func clearFirstHeardOfNewVersionDate() { + self.dbConnection.removeObject(forKey: kFirstHeardOfNewVersionDateKey, inCollection: kUpgradeNagCollection) + } + + var lastNagDate: Date? { + return self.dbConnection.date(forKey: kLastNagDateKey, inCollection: kUpgradeNagCollection) + } + + func setLastNagDate(_ date: Date) { + self.dbConnection.setDate(date, forKey: kLastNagDateKey, inCollection: kUpgradeNagCollection) + } +} + +// MARK: Parsing Structs + +struct AppStoreLookupResultSet: Codable { + let resultCount: UInt + let results: [AppStoreRecord] +} + +struct AppStoreRecord: Codable { + let appStoreURL: URL + let version: String + + private enum CodingKeys: String, CodingKey { + case appStoreURL = "trackViewUrl" + case version + } +} + +class AppStoreVersionService: NSObject { + + // MARK: + + func fetchLatestVersion(lookupURL: URL) -> Promise { + Logger.debug("\(logTag) in \(#function) lookupURL:\(lookupURL)") + + let (promise, fulfill, reject) = Promise.pending() + + let task = URLSession.ephemeral.dataTask(with: lookupURL) { (data, _, error) in + guard let data = data else { + owsFail("\(self.logTag) in \(#function) data was unexpectedly nil") + reject(OWSErrorMakeUnableToProcessServerResponseError()) + return + } + + do { + let decoder = JSONDecoder() + let resultSet = try decoder.decode(AppStoreLookupResultSet.self, from: data) + guard let appStoreRecord = resultSet.results.first else { + owsFail("\(self.logTag) in \(#function) record was unexpectedly nil") + reject(OWSErrorMakeUnableToProcessServerResponseError()) + return + } + + fulfill(appStoreRecord) + } catch { + reject(error) + } + } + + task.resume() + + return promise + } +} + +extension URLSession { + static var ephemeral: URLSession { + return URLSession(configuration: .ephemeral) + } +}