TM-SGNL-iOS/Signal/Expiration/AppUpdateNag.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

215 lines
8.2 KiB
Swift

//
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
import SignalUI
class AppUpdateNag {
// MARK: Public
public static let shared = AppUpdateNag(databaseStorage: SSKEnvironment.shared.databaseStorageRef)
public func showAppUpgradeNagIfNecessary() {
let currentVersion = AppVersionImpl.shared.currentAppVersion
guard let bundleIdentifier = self.bundleIdentifier else {
owsFailDebug("bundleIdentifier was unexpectedly nil")
return
}
guard let lookupURL = lookupURL(bundleIdentifier: bundleIdentifier) else {
owsFailDebug("appStoreURL was unexpectedly nil")
return
}
Task {
do {
let appStoreRecord = try await Self.fetchLatestVersion(lookupURL: lookupURL)
let appStoreVersion = AppVersionNumber(appStoreRecord.version)
let currentVersion = AppVersionNumber(currentVersion)
guard appStoreVersion > currentVersion else {
await self.clearFirstHeardOfNewVersionDate()
return
}
Logger.info("new version available: \(appStoreRecord)")
await self.showUpdateNagIfEnoughTimeHasPassed(appStoreRecord: appStoreRecord)
} catch {
// Only failDebug if we're looking up the true org.whispersystems.signal app store record
// If someone is building Signal with their own bundleID, it's less important that this succeeds.
if error.isNetworkFailureOrTimeout || !bundleIdentifier.hasPrefix("org.whispersystems.") {
Logger.warn("failed with error: \(error)")
} else {
owsFailDebug("Failed to find Signal app store record")
}
}
}
}
private static func fetchLatestVersion(lookupURL: URL) async throws -> AppStoreRecord {
let (data, _) = try await URLSession(configuration: .ephemeral).data(from: lookupURL)
let decoder = JSONDecoder()
let resultSet = try decoder.decode(AppStoreLookupResultSet.self, from: data)
guard let appStoreRecord = resultSet.results.first else {
throw OWSGenericError("Missing or invalid record.")
}
return appStoreRecord
}
// MARK: - Internal
private static let kLastNagDateKey = "TSStorageManagerAppUpgradeNagDate"
private static let kFirstHeardOfNewVersionDateKey = "TSStorageManagerAppUpgradeFirstHeardOfNewVersionDate"
// MARK: - KV Store
private let keyValueStore = KeyValueStore(collection: "TSStorageManagerAppUpgradeNagCollection")
// MARK: - Bundle accessors
private var bundle: Bundle {
return Bundle.main
}
private var bundleIdentifier: String? {
return bundle.bundleIdentifier
}
private func lookupURL(bundleIdentifier: String) -> URL? {
var result = URLComponents(string: "https://itunes.apple.com/lookup")
result?.queryItems = [URLQueryItem(name: "bundleId", value: bundleIdentifier)]
return result?.url
}
private let databaseStorage: SDSDatabaseStorage
private init(databaseStorage: SDSDatabaseStorage) {
self.databaseStorage = databaseStorage
SwiftSingletons.register(self)
}
@MainActor
private func showUpdateNagIfEnoughTimeHasPassed(appStoreRecord: AppStoreRecord) async {
guard let firstHeardOfNewVersionDate = self.firstHeardOfNewVersionDate else {
await setFirstHeardOfNewVersionDate(Date())
return
}
let intervalBeforeNag = 21 * kDayInterval
guard Date() > Date.init(timeInterval: intervalBeforeNag, since: firstHeardOfNewVersionDate) else {
Logger.info("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("lastNagDate: \(lastNagDate) not nagging again so soon.")
return
}
}
// Only show nag if we are "at rest" in the conversation split or registration view without any
// alerts or dialogs showing.
guard let frontmostViewController = UIApplication.shared.frontmostViewController else {
owsFailDebug("frontmostViewController was unexpectedly nil")
return
}
switch frontmostViewController {
case is ConversationSplitViewController, is ProvisioningSplashViewController, is RegistrationSplashViewController:
await setLastNagDate(Date())
await clearFirstHeardOfNewVersionDate()
presentUpgradeNag(appStoreRecord: appStoreRecord)
default:
Logger.debug("not presenting alert due to frontmostViewController: \(frontmostViewController)")
}
}
@MainActor
private func presentUpgradeNag(appStoreRecord: AppStoreRecord) {
let title = OWSLocalizedString("APP_UPDATE_NAG_ALERT_TITLE", comment: "Title for the 'new app version available' alert.")
let bodyFormat = OWSLocalizedString("APP_UPDATE_NAG_ALERT_MESSAGE_FORMAT", comment: "Message format for the 'new app version available' alert. Embeds: {{The latest app version number}}")
let bodyText = String(format: bodyFormat, appStoreRecord.version)
let updateButtonText = OWSLocalizedString("APP_UPDATE_NAG_ALERT_UPDATE_BUTTON", comment: "Label for the 'update' button in the 'new app version available' alert.")
let dismissButtonText = OWSLocalizedString("APP_UPDATE_NAG_ALERT_DISMISS_BUTTON", comment: "Label for the 'dismiss' button in the 'new app version available' alert.")
let alert = ActionSheetController(title: title, message: bodyText)
let updateAction = ActionSheetAction(title: updateButtonText, style: .default) { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.showAppStore(appStoreURL: appStoreRecord.appStoreURL)
}
alert.addAction(updateAction)
alert.addAction(ActionSheetAction(title: dismissButtonText, style: .cancel) { _ in
Logger.info("dismissed upgrade notice")
})
OWSActionSheets.showActionSheet(alert)
}
private func showAppStore(appStoreURL: URL) {
assert(CurrentAppContext().isMainApp)
Logger.debug("")
UIApplication.shared.open(appStoreURL, options: [:])
}
// MARK: Storage
private var firstHeardOfNewVersionDate: Date? {
return databaseStorage.read { transaction in
return self.keyValueStore.getDate(AppUpdateNag.kFirstHeardOfNewVersionDateKey, transaction: transaction.asV2Read)
}
}
private func setFirstHeardOfNewVersionDate(_ date: Date) async {
await databaseStorage.awaitableWrite { transaction in
self.keyValueStore.setDate(date, key: AppUpdateNag.kFirstHeardOfNewVersionDateKey, transaction: transaction.asV2Write)
}
}
private func clearFirstHeardOfNewVersionDate() async {
await databaseStorage.awaitableWrite { transaction in
self.keyValueStore.removeValue(forKey: AppUpdateNag.kFirstHeardOfNewVersionDateKey, transaction: transaction.asV2Write)
}
}
private var lastNagDate: Date? {
return databaseStorage.read { transaction in
return self.keyValueStore.getDate(AppUpdateNag.kLastNagDateKey, transaction: transaction.asV2Read)
}
}
private func setLastNagDate(_ date: Date) async {
await databaseStorage.awaitableWrite { transaction in
self.keyValueStore.setDate(date, key: AppUpdateNag.kLastNagDateKey, transaction: transaction.asV2Write)
}
}
}
// MARK: Parsing Structs
private struct AppStoreLookupResultSet: Codable {
let resultCount: UInt
let results: [AppStoreRecord]
}
private struct AppStoreRecord: Codable {
let appStoreURL: URL
let version: String
private enum CodingKeys: String, CodingKey {
case appStoreURL = "trackViewUrl"
case version
}
}