From 7defe3aa06011294ada65dc02abb0b97979afd16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Wed, 15 Jan 2025 15:15:27 +0100 Subject: [PATCH 1/8] Remove old app delegate and rename states and events --- DuckDuckGo.xcodeproj/project.pbxproj | 44 +- DuckDuckGo/AppDelegate+AppDeepLinks.swift | 4 +- DuckDuckGo/AppDelegate.swift | 77 +- DuckDuckGo/AppLifecycle/AppStateMachine.swift | 11 +- .../AppLifecycle/AppStateTransitions.swift | 82 +- .../AppLifecycle/AppStates/Background.swift | 21 +- .../{Active.swift => Foreground.swift} | 33 +- .../{Init.swift => Initializing.swift} | 6 +- .../{Launched.swift => Launching.swift} | 8 +- .../AppLifecycle/AppStates/Resuming.swift | 107 ++ .../{Inactive.swift => Suspending.swift} | 11 +- DuckDuckGo/AppSettings.swift | 2 - DuckDuckGo/AppUserDefaults.swift | 13 - DuckDuckGo/MainViewController.swift | 13 +- DuckDuckGo/NewAppDelegate.swift | 66 - DuckDuckGo/OldAppDelegate.swift | 1265 ----------------- 16 files changed, 250 insertions(+), 1513 deletions(-) rename DuckDuckGo/AppLifecycle/AppStates/{Active.swift => Foreground.swift} (96%) rename DuckDuckGo/AppLifecycle/AppStates/{Init.swift => Initializing.swift} (94%) rename DuckDuckGo/AppLifecycle/AppStates/{Launched.swift => Launching.swift} (99%) create mode 100644 DuckDuckGo/AppLifecycle/AppStates/Resuming.swift rename DuckDuckGo/AppLifecycle/AppStates/{Inactive.swift => Suspending.swift} (84%) delete mode 100644 DuckDuckGo/NewAppDelegate.swift delete mode 100644 DuckDuckGo/OldAppDelegate.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index affb71c084..50f876516a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1010,13 +1010,13 @@ CB2A7EEF283D185100885F67 /* RulesCompilationMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2A7EEE283D185100885F67 /* RulesCompilationMonitor.swift */; }; CB2A7EF128410DF700885F67 /* PixelEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2A7EF028410DF700885F67 /* PixelEvent.swift */; }; CB2A7EF4285383B300885F67 /* AppLastCompiledRulesStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2A7EF3285383B300885F67 /* AppLastCompiledRulesStore.swift */; }; - CB3C78892D06D3A700A7E4ED /* Active.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFC2CFE1D48006267B8 /* Active.swift */; }; + CB3C78892D06D3A700A7E4ED /* Foreground.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFC2CFE1D48006267B8 /* Foreground.swift */; }; CB3C788A2D06D3A700A7E4ED /* Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F002CFE1D54006267B8 /* Background.swift */; }; - CB3C788B2D06D3A700A7E4ED /* Launched.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFA2CFE1D3F006267B8 /* Launched.swift */; }; + CB3C788B2D06D3A700A7E4ED /* Launching.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFA2CFE1D3F006267B8 /* Launching.swift */; }; CB3C788C2D06D3A700A7E4ED /* AppStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F052CFE270D006267B8 /* AppStateMachine.swift */; }; CB3C788D2D06D3A700A7E4ED /* AppStateTransitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */; }; - CB3C788E2D06D3A700A7E4ED /* Init.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EF82CFE1D35006267B8 /* Init.swift */; }; - CB3C788F2D06D3A700A7E4ED /* Inactive.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFE2CFE1D4E006267B8 /* Inactive.swift */; }; + CB3C788E2D06D3A700A7E4ED /* Initializing.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EF82CFE1D35006267B8 /* Initializing.swift */; }; + CB3C788F2D06D3A700A7E4ED /* Suspending.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFE2CFE1D4E006267B8 /* Suspending.swift */; }; CB48D3332B90CE9F00631D8B /* PageRefreshStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB48D3312B90CE9F00631D8B /* PageRefreshStore.swift */; }; CB4FA44E2C78AACE00A16F5A /* SpecialErrorPageUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB4FA44D2C78AACE00A16F5A /* SpecialErrorPageUserScript.swift */; }; CB5516D0286500290079B175 /* TrackerRadarIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85519124247468580010FDD0 /* TrackerRadarIntegrationTests.swift */; }; @@ -1044,14 +1044,13 @@ CBD4F13E279EBFAB00B20FD7 /* HomeMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF14FC227970072001D94D0 /* HomeMessageView.swift */; }; CBD4F13F279EBFAF00B20FD7 /* HomeMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF14FC427970AB0001D94D0 /* HomeMessageViewModel.swift */; }; CBD4F140279EBFB300B20FD7 /* SwiftUICollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB1AEFB02799AA940031AE3D /* SwiftUICollectionViewCell.swift */; }; - CBD79F482D1061DA00DBB45A /* NewAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD79F472D1061D500DBB45A /* NewAppDelegate.swift */; }; - CBD79F4A2D1061E200DBB45A /* OldAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD79F492D1061DF00DBB45A /* OldAppDelegate.swift */; }; CBD79F4D2D130F6500DBB45A /* Testing.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD79F4C2D130F6300DBB45A /* Testing.swift */; }; CBDD5DDF29A6736A00832877 /* APIHeadersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD5DDE29A6736A00832877 /* APIHeadersTests.swift */; }; CBDD5DE129A6741300832877 /* MockBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD5DE029A6741300832877 /* MockBundle.swift */; }; CBECDB6F2CD3DFBE005B8B87 /* PageRefreshMonitor in Frameworks */ = {isa = PBXBuildFile; productRef = CBECDB6E2CD3DFBE005B8B87 /* PageRefreshMonitor */; }; CBECDB7A2CD981CE005B8B87 /* AppPageRefreshMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBECDB792CD981C6005B8B87 /* AppPageRefreshMonitor.swift */; }; CBEFB9142AE0844700DEDE7B /* CriticalAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEFB9102ADFFE7900DEDE7B /* CriticalAlerts.swift */; }; + CBF259502D37ED6600AC63E4 /* Resuming.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF2594F2D37ED6100AC63E4 /* Resuming.swift */; }; CBFCB30E2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFCB30D2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift */; }; D60170BD2BA34CE8001911B5 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60170BB2BA32DD6001911B5 /* Subscription.swift */; }; D6037E692C32F2E7009AAEC0 /* DuckPlayerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6037E682C32F2E7009AAEC0 /* DuckPlayerSettings.swift */; }; @@ -2960,10 +2959,10 @@ CBA1DE942AF6D579007C9457 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/InfoPlist.strings; sourceTree = ""; }; CBAA195927BFE15600A4BD49 /* NSManagedObjectContextExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContextExtension.swift; sourceTree = ""; }; CBAA195B27C3982A00A4BD49 /* PrivacyFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyFeatures.swift; sourceTree = ""; }; - CBAD0EF82CFE1D35006267B8 /* Init.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Init.swift; sourceTree = ""; }; - CBAD0EFA2CFE1D3F006267B8 /* Launched.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Launched.swift; sourceTree = ""; }; - CBAD0EFC2CFE1D48006267B8 /* Active.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Active.swift; sourceTree = ""; }; - CBAD0EFE2CFE1D4E006267B8 /* Inactive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Inactive.swift; sourceTree = ""; }; + CBAD0EF82CFE1D35006267B8 /* Initializing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Initializing.swift; sourceTree = ""; }; + CBAD0EFA2CFE1D3F006267B8 /* Launching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Launching.swift; sourceTree = ""; }; + CBAD0EFC2CFE1D48006267B8 /* Foreground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Foreground.swift; sourceTree = ""; }; + CBAD0EFE2CFE1D4E006267B8 /* Suspending.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Suspending.swift; sourceTree = ""; }; CBAD0F002CFE1D54006267B8 /* Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Background.swift; sourceTree = ""; }; CBAD0F052CFE270D006267B8 /* AppStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateMachine.swift; sourceTree = ""; }; CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateTransitions.swift; sourceTree = ""; }; @@ -2978,8 +2977,6 @@ CBC88EE42C8097B500F0F8C5 /* URLCredentialCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLCredentialCreator.swift; sourceTree = ""; }; CBC8DC252AF6D4CD00BA681A /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/InfoPlist.strings; sourceTree = ""; }; CBD4F13B279EBF4A00B20FD7 /* HomeMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeMessage.swift; sourceTree = ""; }; - CBD79F472D1061D500DBB45A /* NewAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewAppDelegate.swift; sourceTree = ""; }; - CBD79F492D1061DF00DBB45A /* OldAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OldAppDelegate.swift; sourceTree = ""; }; CBD79F4C2D130F6300DBB45A /* Testing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Testing.swift; sourceTree = ""; }; CBD7AE812AF6D5B6009052FD /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = ""; }; CBDD5DDE29A6736A00832877 /* APIHeadersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIHeadersTests.swift; sourceTree = ""; }; @@ -2993,6 +2990,7 @@ CBF14FC227970072001D94D0 /* HomeMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeMessageView.swift; sourceTree = ""; }; CBF14FC427970AB0001D94D0 /* HomeMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeMessageViewModel.swift; sourceTree = ""; }; CBF14FC627970C8A001D94D0 /* HomeMessageCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeMessageCollectionViewCell.swift; sourceTree = ""; }; + CBF2594F2D37ED6100AC63E4 /* Resuming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resuming.swift; sourceTree = ""; }; CBFCB30D2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationURLDebugViewController.swift; sourceTree = ""; }; D60170BB2BA32DD6001911B5 /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = ""; }; D6037E682C32F2E7009AAEC0 /* DuckPlayerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayerSettings.swift; sourceTree = ""; }; @@ -5748,11 +5746,12 @@ CBAD0EF72CFE1D14006267B8 /* AppStates */ = { isa = PBXGroup; children = ( - CBAD0EF82CFE1D35006267B8 /* Init.swift */, - CBAD0EFA2CFE1D3F006267B8 /* Launched.swift */, - CBAD0EFC2CFE1D48006267B8 /* Active.swift */, - CBAD0EFE2CFE1D4E006267B8 /* Inactive.swift */, + CBAD0EF82CFE1D35006267B8 /* Initializing.swift */, + CBAD0EFA2CFE1D3F006267B8 /* Launching.swift */, + CBAD0EFC2CFE1D48006267B8 /* Foreground.swift */, + CBAD0EFE2CFE1D4E006267B8 /* Suspending.swift */, CBAD0F002CFE1D54006267B8 /* Background.swift */, + CBF2594F2D37ED6100AC63E4 /* Resuming.swift */, CBD79F4C2D130F6300DBB45A /* Testing.swift */, ); path = AppStates; @@ -6678,8 +6677,6 @@ 83BE9BC2215D69C1009844D9 /* AppConfigurationFetch.swift */, CB24F70E29A3EB15006DCC58 /* AppConfigurationURLProvider.swift */, 84E341951E2F7EFB00BDBA6F /* AppDelegate.swift */, - CBD79F492D1061DF00DBB45A /* OldAppDelegate.swift */, - CBD79F472D1061D500DBB45A /* NewAppDelegate.swift */, 85DB12EC2A1FED0C000A4A72 /* AppDelegate+AppDeepLinks.swift */, 98B31291218CCB8C00E54DE1 /* AppDependencyProvider.swift */, 85BA58591F3506AE00C6E8CA /* AppSettings.swift */, @@ -7878,7 +7875,6 @@ B623C1C42862CD670043013E /* WKDownloadSession.swift in Sources */, 6FD1BAE42B87A107000C475C /* AdAttributionPixelReporter.swift in Sources */, 1E8AD1D927C4FEC100ABA377 /* DownloadsListSectioningHelper.swift in Sources */, - CBD79F4A2D1061E200DBB45A /* OldAppDelegate.swift in Sources */, D60170BD2BA34CE8001911B5 /* Subscription.swift in Sources */, 1E4DCF4827B6A35400961E25 /* DownloadsListModel.swift in Sources */, C12726F02A5FF89900215B02 /* EmailSignupPromptViewModel.swift in Sources */, @@ -7933,15 +7929,14 @@ 9FEA22272C2D2BDA006B03BF /* RootDebugViewController+Onboarding.swift in Sources */, 319A37152829A55F0079FBCE /* AutofillListItemTableViewCell.swift in Sources */, 6F8496412BC3D8EE00ADA54E /* OnboardingButtonsView.swift in Sources */, - CB3C78892D06D3A700A7E4ED /* Active.swift in Sources */, + CB3C78892D06D3A700A7E4ED /* Foreground.swift in Sources */, CB3C788A2D06D3A700A7E4ED /* Background.swift in Sources */, - CB3C788B2D06D3A700A7E4ED /* Launched.swift in Sources */, + CB3C788B2D06D3A700A7E4ED /* Launching.swift in Sources */, CB3C788C2D06D3A700A7E4ED /* AppStateMachine.swift in Sources */, CB3C788D2D06D3A700A7E4ED /* AppStateTransitions.swift in Sources */, - CB3C788E2D06D3A700A7E4ED /* Init.swift in Sources */, - CB3C788F2D06D3A700A7E4ED /* Inactive.swift in Sources */, + CB3C788E2D06D3A700A7E4ED /* Initializing.swift in Sources */, + CB3C788F2D06D3A700A7E4ED /* Suspending.swift in Sources */, 1EA513782866039400493C6A /* TrackerAnimationLogic.swift in Sources */, - CBD79F482D1061DA00DBB45A /* NewAppDelegate.swift in Sources */, 854A01332A558B3A00FCC628 /* UIView+Constraints.swift in Sources */, 9FB0271B2C2927D0009EA190 /* OnboardingView.swift in Sources */, C12726EE2A5FF88C00215B02 /* EmailSignupPromptView.swift in Sources */, @@ -8006,6 +8001,7 @@ 1E1626072968413B0004127F /* ViewExtension.swift in Sources */, 31A42566285A0A6300049386 /* FaviconViewModel.swift in Sources */, 6F691CCA2C4979EC002E9553 /* FavoritesTooltip.swift in Sources */, + CBF259502D37ED6600AC63E4 /* Resuming.swift in Sources */, 859DB8132CE6263C001F7210 /* TextZoomStorage.swift in Sources */, D65625952C22D382006EF297 /* TabViewController.swift in Sources */, 8C4838B5221C8F7F008A6739 /* GestureToolbarButton.swift in Sources */, diff --git a/DuckDuckGo/AppDelegate+AppDeepLinks.swift b/DuckDuckGo/AppDelegate+AppDeepLinks.swift index 8b59faa969..27a7eb472f 100644 --- a/DuckDuckGo/AppDelegate+AppDeepLinks.swift +++ b/DuckDuckGo/AppDelegate+AppDeepLinks.swift @@ -20,7 +20,7 @@ import UIKit import Core -extension OldAppDelegate { +extension AppDelegate { func handleAppDeepLink(_ app: UIApplication, _ mainViewController: MainViewController?, _ url: URL) -> Bool { guard let mainViewController else { return false } @@ -51,7 +51,7 @@ extension OldAppDelegate { mainViewController.newEmailAddress() case .openVPN: - presentNetworkProtectionStatusSettingsModal() + mainViewController.presentNetworkProtectionStatusSettingsModal() case .openPasswords: var source: AutofillSettingsSource = .homeScreenWidget diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 64f735503f..949a839715 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -20,22 +20,6 @@ import UIKit import Core -enum AppBehavior: String { - - case old - case new - -} - -protocol DDGApp { - - var privacyProDataReporter: PrivacyProDataReporting? { get } - - func initialize() - func refreshRemoteMessages() - -} - @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { static let ShowKeyboardOnLaunchThreshold = TimeInterval(20) @@ -45,65 +29,45 @@ protocol DDGApp { static let openVPNSettings = "com.duckduckgo.mobile.ios.vpn.open-settings" } + private let appStateMachine: AppStateMachine = AppStateMachine() + var window: UIWindow? var privacyProDataReporter: PrivacyProDataReporting? { - realDelegate.privacyProDataReporter - } - - func forceOldAppDelegate() { - BoolFileMarker(name: .forceOldAppDelegate)?.mark() - } - - private let appBehavior: AppBehavior = { - BoolFileMarker(name: .forceOldAppDelegate)?.isPresent == true ? .old : .new - }() - - private lazy var realDelegate: UIApplicationDelegate & DDGApp = { - if appBehavior == .old { - return OldAppDelegate(with: self) - } else { - return NewAppDelegate() - } - }() - - var didCallWillEnterForeground: Bool = false - - override init() { - super.init() - realDelegate.initialize() + (appStateMachine.currentState as? Foreground)?.appDependencies.privacyProDataReporter // just for now, we have to get rid of this anti pattern } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - realDelegate.application?(application, didFinishLaunchingWithOptions: launchOptions) ?? false + let isTesting: Bool = ProcessInfo().arguments.contains("testing") + appStateMachine.handle(.didFinishLaunching(application, isTesting: isTesting)) + return true } func applicationDidBecomeActive(_ application: UIApplication) { - didCallWillEnterForeground = false - realDelegate.applicationDidBecomeActive?(application) + appStateMachine.handle(.didBecomeActive) } func applicationWillResignActive(_ application: UIApplication) { - realDelegate.applicationWillResignActive?(application) + appStateMachine.handle(.willResignActive) } func applicationWillEnterForeground(_ application: UIApplication) { - didCallWillEnterForeground = true - realDelegate.applicationWillEnterForeground?(application) + appStateMachine.handle(.willEnterForeground) } func applicationDidEnterBackground(_ application: UIApplication) { - realDelegate.applicationDidEnterBackground?(application) + appStateMachine.handle(.didEnterBackground) } func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { - realDelegate.application?(application, performActionFor: shortcutItem, completionHandler: completionHandler) + appStateMachine.handle(.handleShortcutItem(shortcutItem)) } func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { - realDelegate.application?(app, open: url, options: options) ?? false + appStateMachine.handle(.openURL(url)) + return true } func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { @@ -121,12 +85,12 @@ protocol DDGApp { } func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool { - return true + true } /// It's public in order to allow refreshing on demand via Debug menu. Otherwise it shouldn't be called from outside. func refreshRemoteMessages() { - realDelegate.refreshRemoteMessages() + // part of debug menu, let's not support it in the first iteration } } @@ -153,19 +117,10 @@ extension Error { let nsError = self as NSError if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError, underlyingError.code == 13 { return true - } - - if nsError.userInfo["NSSQLiteErrorDomain"] as? Int == 13 { + } else if nsError.userInfo["NSSQLiteErrorDomain"] as? Int == 13 { return true } - return false } } - -private extension BoolFileMarker.Name { - - static let forceOldAppDelegate = BoolFileMarker.Name(rawValue: "force-old-app-delegate") - -} diff --git a/DuckDuckGo/AppLifecycle/AppStateMachine.swift b/DuckDuckGo/AppLifecycle/AppStateMachine.swift index 585f9bfb7c..abe4d57f17 100644 --- a/DuckDuckGo/AppLifecycle/AppStateMachine.swift +++ b/DuckDuckGo/AppLifecycle/AppStateMachine.swift @@ -21,10 +21,11 @@ import UIKit enum AppEvent { - case launching(UIApplication, isTesting: Bool) - case activating - case backgrounding - case suspending + case didFinishLaunching(UIApplication, isTesting: Bool) + case didBecomeActive + case didEnterBackground + case willResignActive + case willEnterForeground case openURL(URL) case handleShortcutItem(UIApplicationShortcutItem) @@ -47,7 +48,7 @@ protocol AppEventHandler { @MainActor final class AppStateMachine: AppEventHandler { - private(set) var currentState: any AppState = Init() + private(set) var currentState: any AppState = Initializing() func handle(_ event: AppEvent) { currentState = currentState.apply(event: event) diff --git a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift index d3213f7884..e25ceb8bb0 100644 --- a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift +++ b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift @@ -20,15 +20,15 @@ import os.log import Core -extension Init { +extension Initializing { func apply(event: AppEvent) -> any AppState { switch event { - case .launching(let application, let isTesting): + case .didFinishLaunching(let application, let isTesting): if isTesting { return Testing(application: application) } - return Launched(stateContext: makeStateContext(application: application)) + return Launching(stateContext: makeStateContext(application: application)) default: return handleUnexpectedEvent(event) } @@ -36,58 +36,61 @@ extension Init { } -extension Launched { +extension Launching { mutating func apply(event: AppEvent) -> any AppState { switch event { - case .activating: - return Active(stateContext: makeStateContext()) + case .didBecomeActive: + return Foreground(stateContext: makeStateContext()) + case .didEnterBackground: + return Background(stateContext: makeStateContext()) case .openURL(let url): urlToOpen = url return self case .handleShortcutItem(let shortcutItem): shortcutItemToHandle = shortcutItem return self - case .backgrounding: - return Background(stateContext: makeStateContext()) - case .launching, .suspending: + case .didFinishLaunching, .willResignActive, .willEnterForeground: return handleUnexpectedEvent(event) } } } -extension Active { +extension Foreground { func apply(event: AppEvent) -> any AppState { switch event { - case .suspending: - return Inactive(stateContext: makeStateContext()) + case .willResignActive: + return Suspending(stateContext: makeStateContext()) case .openURL(let url): openURL(url) return self case .handleShortcutItem(let shortcutItem): handleShortcutItem(shortcutItem) return self - case .launching, .activating, .backgrounding: + case .didFinishLaunching, .didBecomeActive, .didEnterBackground, .willEnterForeground: return handleUnexpectedEvent(event) } } } -extension Inactive { +extension Suspending { mutating func apply(event: AppEvent) -> any AppState { switch event { - case .backgrounding: + case .didEnterBackground: return Background(stateContext: makeStateContext()) - case .activating: - return Active(stateContext: makeStateContext()) + case .didBecomeActive: + return Foreground(stateContext: makeStateContext()) case .openURL(let url): urlToOpen = url return self - case .launching, .suspending, .handleShortcutItem: + case .handleShortcutItem(let shortcutItem): + shortcutItemToHandle = shortcutItem + return self + case .didFinishLaunching, .willResignActive, .willEnterForeground: return handleUnexpectedEvent(event) } } @@ -98,24 +101,36 @@ extension Background { mutating func apply(event: AppEvent) -> any AppState { switch event { - case .activating: - return Active(stateContext: makeStateContext()) + case .willEnterForeground: + return Resuming(stateContext: makeStateContext()) case .openURL(let url): urlToOpen = url return self - case .backgrounding: - if let appDelegate = UIApplication.shared.delegate as? AppDelegate { - Pixel.fire(pixel: .appDidConsecutivelyBackground, withAdditionalParameters: [ - PixelParameters.didCallWillEnterForeground: appDelegate.didCallWillEnterForeground.description - ]) - appDelegate.didCallWillEnterForeground = false - } - run() + case .handleShortcutItem(let shortcutItem): + shortcutItemToHandle = shortcutItem + return self + case .didFinishLaunching, .didBecomeActive, .willResignActive, .didEnterBackground: + return handleUnexpectedEvent(event) + } + } + +} + +extension Resuming { + + mutating func apply(event: AppEvent) -> any AppState { + switch event { + case .didBecomeActive: + return Foreground(stateContext: makeStateContext()) + case .didEnterBackground: + return Background(stateContext: makeStateContext()) + case .openURL(let url): + urlToOpen = url return self case .handleShortcutItem(let shortcutItem): shortcutItemToHandle = shortcutItem return self - case .launching, .suspending: + case .didFinishLaunching, .willResignActive, .willEnterForeground: return handleUnexpectedEvent(event) } } @@ -132,10 +147,11 @@ extension AppEvent { var rawValue: String { switch self { - case .launching: return "launching" - case .activating: return "activating" - case .backgrounding: return "backgrounding" - case .suspending: return "suspending" + case .didFinishLaunching: return "launching" + case .didBecomeActive: return "activating" + case .didEnterBackground: return "backgrounding" + case .willResignActive: return "suspending" + case .willEnterForeground: return "resuming" case .openURL: return "openURL" case .handleShortcutItem: return "handleShortcutItem" } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Background.swift b/DuckDuckGo/AppLifecycle/AppStates/Background.swift index f408b0919d..d3959f900e 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Background.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Background.swift @@ -32,7 +32,7 @@ struct Background: AppState { var urlToOpen: URL? var shortcutItemToHandle: UIApplicationShortcutItem? - init(stateContext: Inactive.StateContext) { + init(stateContext: Suspending.StateContext) { application = stateContext.application appDependencies = stateContext.appDependencies urlToOpen = stateContext.urlToOpen @@ -40,7 +40,15 @@ struct Background: AppState { run() } - init(stateContext: Launched.StateContext) { + init(stateContext: Launching.StateContext) { + application = stateContext.application + appDependencies = stateContext.appDependencies + urlToOpen = stateContext.urlToOpen + + run() + } + + init(stateContext: Resuming.StateContext) { application = stateContext.application appDependencies = stateContext.appDependencies urlToOpen = stateContext.urlToOpen @@ -71,15 +79,6 @@ struct Background: AppState { privacyProDataReporter.saveApplicationLastSessionEnded() resetAppStartTime() - - // Kill switch for the new app delegate: - // If the .forceOldAppDelegate flag is set in the config, we mark a file as present. - // This switches the app to the old mode and silently crashes it in the background. - // When reopened, the app will reliably run the old flow. - if ContentBlocking.shared.privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .forceOldAppDelegate) { - (UIApplication.shared.delegate as? AppDelegate)?.forceOldAppDelegate() - fatalError("crash to ensure the app restarts using the old app delegate next time") - } } private mutating func suspendSync(syncService: DDGSync) { diff --git a/DuckDuckGo/AppLifecycle/AppStates/Active.swift b/DuckDuckGo/AppLifecycle/AppStates/Foreground.swift similarity index 96% rename from DuckDuckGo/AppLifecycle/AppStates/Active.swift rename to DuckDuckGo/AppLifecycle/AppStates/Foreground.swift index 42afc268c4..c2188cfed0 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Active.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Foreground.swift @@ -1,5 +1,5 @@ // -// Active.swift +// Foreground.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -26,7 +26,7 @@ import BackgroundTasks import Subscription import NetworkProtection -struct Active: AppState { +struct Foreground: AppState { let application: UIApplication let appDependencies: AppDependencies @@ -43,7 +43,7 @@ struct Active: AppState { } // MARK: handle one-time (after launch) logic here - init(stateContext: Launched.StateContext) { + init(stateContext: Launching.StateContext) { application = stateContext.application appDependencies = stateContext.appDependencies @@ -75,6 +75,7 @@ struct Active: AppState { refreshRemoteMessages(remoteMessagingClient: appDependencies.remoteMessagingClient) } + // TODO: it should happen after autoclear if let url = stateContext.urlToOpen { openURL(url) } else if let shortcutItemToHandle = stateContext.shortcutItemToHandle { @@ -84,23 +85,11 @@ struct Active: AppState { activateApp() } - // MARK: handle applicationWillEnterForeground(_:) logic here - init(stateContext: Background.StateContext) { + init(stateContext: Resuming.StateContext) { application = stateContext.application appDependencies = stateContext.appDependencies - ThemeManager.shared.updateUserInterfaceStyle() - - let uiService = appDependencies.uiService - let syncService = appDependencies.syncService - let autoClear = appDependencies.autoClear - Task { @MainActor [self] in - await beginAuthentication(lastBackgroundDate: stateContext.lastBackgroundDate) - await autoClear.clearDataIfEnabledAndTimeExpired(applicationState: .active) - uiService.showKeyboardIfSettingOn = true - syncService.scheduler.resumeSyncQueue() - } - + // TODO: it should happen after autoclear if let url = stateContext.urlToOpen { openURL(url) } else if let shortcutItemToHandle = stateContext.shortcutItemToHandle { @@ -110,10 +99,16 @@ struct Active: AppState { activateApp() } - init(stateContext: Inactive.StateContext) { + init(stateContext: Suspending.StateContext) { application = stateContext.application appDependencies = stateContext.appDependencies + if let url = stateContext.urlToOpen { + openURL(url) + } else if let shortcutItemToHandle = stateContext.shortcutItemToHandle { + handleShortcutItem(shortcutItemToHandle, appIsLaunching: false) + } + activateApp() } @@ -497,7 +492,7 @@ struct Active: AppState { } -extension Active { +extension Foreground { struct StateContext { diff --git a/DuckDuckGo/AppLifecycle/AppStates/Init.swift b/DuckDuckGo/AppLifecycle/AppStates/Initializing.swift similarity index 94% rename from DuckDuckGo/AppLifecycle/AppStates/Init.swift rename to DuckDuckGo/AppLifecycle/AppStates/Initializing.swift index 5054d8f27e..66cc420bed 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Init.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Initializing.swift @@ -1,5 +1,5 @@ // -// Init.swift +// Initializing.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -22,7 +22,7 @@ import Crashes import UIKit @MainActor -struct Init: AppState { +struct Initializing: AppState { @UserDefaultsWrapper(key: .didCrashDuringCrashHandlersSetUp, defaultValue: false) var didCrashDuringCrashHandlersSetUp: Bool @@ -37,7 +37,7 @@ struct Init: AppState { } -extension Init { +extension Initializing { struct StateContext { diff --git a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift b/DuckDuckGo/AppLifecycle/AppStates/Launching.swift similarity index 99% rename from DuckDuckGo/AppLifecycle/AppStates/Launched.swift rename to DuckDuckGo/AppLifecycle/AppStates/Launching.swift index 981e1bb5c8..cc2738ef04 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Launching.swift @@ -1,5 +1,5 @@ // -// Launched.swift +// Launching.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -36,7 +36,7 @@ import PixelKit import PixelExperimentKit @MainActor -struct Launched: AppState { +struct Launching: AppState { @UserDefaultsWrapper(key: .didCrashDuringCrashHandlersSetUp, defaultValue: false) private var didCrashDuringCrashHandlersSetUp: Bool @@ -84,7 +84,7 @@ struct Launched: AppState { private let application: UIApplication // swiftlint:disable:next cyclomatic_complexity - init(stateContext: Init.StateContext) { + init(stateContext: Initializing.StateContext) { @UserDefaultsWrapper(key: .privacyConfigCustomURL, defaultValue: nil) var privacyConfigCustomURL: String? @@ -618,7 +618,7 @@ struct Launched: AppState { } -extension Launched { +extension Launching { struct StateContext { diff --git a/DuckDuckGo/AppLifecycle/AppStates/Resuming.swift b/DuckDuckGo/AppLifecycle/AppStates/Resuming.swift new file mode 100644 index 0000000000..57b11efc79 --- /dev/null +++ b/DuckDuckGo/AppLifecycle/AppStates/Resuming.swift @@ -0,0 +1,107 @@ +// +// Resuming.swift +// DuckDuckGo +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +struct Resuming: AppState { + + private let application: UIApplication + private let appDependencies: AppDependencies + + var urlToOpen: URL? + var shortcutItemToHandle: UIApplicationShortcutItem? + + init(stateContext: Background.StateContext) { + application = stateContext.application + appDependencies = stateContext.appDependencies + + ThemeManager.shared.updateUserInterfaceStyle() + + let uiService = appDependencies.uiService + let syncService = appDependencies.syncService + let autoClear = appDependencies.autoClear + Task { @MainActor [self] in + await beginAuthentication(lastBackgroundDate: stateContext.lastBackgroundDate) + await autoClear.clearDataIfEnabledAndTimeExpired(applicationState: .active) + uiService.showKeyboardIfSettingOn = true + syncService.scheduler.resumeSyncQueue() + } + + } + + // duplicated from Foreground just for now + + @MainActor + private func beginAuthentication(lastBackgroundDate: Date? = nil) async { + guard appDependencies.privacyStore.authenticationEnabled else { return } + + let uiService = appDependencies.uiService + uiService.removeOverlay() + uiService.displayAuthenticationWindow() + + guard let controller = uiService.overlayWindow?.rootViewController as? AuthenticationViewController else { + uiService.removeOverlay() + return + } + + await controller.beginAuthentication { + uiService.removeOverlay() + showKeyboardOnLaunch(lastBackgroundDate: lastBackgroundDate) + } + } + + private func showKeyboardOnLaunch(lastBackgroundDate: Date? = nil) { + guard KeyboardSettings().onAppLaunch && appDependencies.uiService.showKeyboardIfSettingOn && shouldShowKeyboardOnLaunch(lastBackgroundDate: lastBackgroundDate) else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.mainViewController.enterSearch() + } + appDependencies.uiService.showKeyboardIfSettingOn = false + } + + private func shouldShowKeyboardOnLaunch(lastBackgroundDate: Date? = nil) -> Bool { + guard let lastBackgroundDate else { return true } + return Date().timeIntervalSince(lastBackgroundDate) > AppDelegate.ShowKeyboardOnLaunchThreshold + } + + private var mainViewController: MainViewController { + appDependencies.mainViewController + } + +} + +extension Resuming { + + struct StateContext { + + let application: UIApplication + let urlToOpen: URL? + let shortcutItemToHandle: UIApplicationShortcutItem? + let appDependencies: AppDependencies + + } + + func makeStateContext() -> StateContext { + .init(application: application, + urlToOpen: urlToOpen, + shortcutItemToHandle: shortcutItemToHandle, + appDependencies: appDependencies) + } + +} + diff --git a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift b/DuckDuckGo/AppLifecycle/AppStates/Suspending.swift similarity index 84% rename from DuckDuckGo/AppLifecycle/AppStates/Inactive.swift rename to DuckDuckGo/AppLifecycle/AppStates/Suspending.swift index 10837bd2c8..f79031bfd7 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Suspending.swift @@ -1,5 +1,5 @@ // -// Inactive.swift +// Suspending.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -19,14 +19,15 @@ import UIKit -struct Inactive: AppState { +struct Suspending: AppState { private let application: UIApplication private let appDependencies: AppDependencies var urlToOpen: URL? + var shortcutItemToHandle: UIApplicationShortcutItem? - init(stateContext: Active.StateContext) { + init(stateContext: Foreground.StateContext) { application = stateContext.application appDependencies = stateContext.appDependencies @@ -42,12 +43,13 @@ struct Inactive: AppState { } -extension Inactive { +extension Suspending { struct StateContext { let application: UIApplication let urlToOpen: URL? + let shortcutItemToHandle: UIApplicationShortcutItem? let appDependencies: AppDependencies } @@ -55,6 +57,7 @@ extension Inactive { func makeStateContext() -> StateContext { .init(application: application, urlToOpen: urlToOpen, + shortcutItemToHandle: shortcutItemToHandle, appDependencies: appDependencies) } diff --git a/DuckDuckGo/AppSettings.swift b/DuckDuckGo/AppSettings.swift index 106a92d9f9..f7242d9a88 100644 --- a/DuckDuckGo/AppSettings.swift +++ b/DuckDuckGo/AppSettings.swift @@ -84,8 +84,6 @@ protocol AppSettings: AnyObject, AppDebugSettings { var duckPlayerMode: DuckPlayerMode { get set } var duckPlayerAskModeOverlayHidden: Bool { get set } var duckPlayerOpenInNewTab: Bool { get set } - - var appBehavior: AppBehavior? { get set } } protocol AppDebugSettings { diff --git a/DuckDuckGo/AppUserDefaults.swift b/DuckDuckGo/AppUserDefaults.swift index 6c4d9ad876..598df50e3c 100644 --- a/DuckDuckGo/AppUserDefaults.swift +++ b/DuckDuckGo/AppUserDefaults.swift @@ -80,8 +80,6 @@ public class AppUserDefaults: AppSettings { static let duckPlayerMode = "com.duckduckgo.ios.duckPlayerMode" static let duckPlayerAskModeOverlayHidden = "com.duckduckgo.ios.duckPlayerAskModeOverlayHidden" static let duckPlayerOpenInNewTab = "com.duckduckgo.ios.duckPlayerOpenInNewTab" - - static let appBehavior = "com.duckduckgo.ios.appBehavior" } private struct DebugKeys { @@ -149,17 +147,6 @@ public class AppUserDefaults: AppSettings { } - var appBehavior: AppBehavior? { - get { - let value = userDefaults?.string(forKey: Keys.appBehavior) ?? "" - return AppBehavior(rawValue: value) - } - - set { - userDefaults?.setValue(newValue?.rawValue, forKey: Keys.appBehavior) - } - } - var autoClearAction: AutoClearSettingsModel.Action { get { diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index a1d1b05364..ec1bc3d58e 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -504,7 +504,18 @@ class MainViewController: UIViewController { segueToDaxOnboarding() } - + + func presentNetworkProtectionStatusSettingsModal() { + Task { + let accountManager = AppDependencyProvider.shared.subscriptionManager.accountManager + if case .success(let hasEntitlements) = await accountManager.hasEntitlement(forProductName: .networkProtection), hasEntitlements { + segueToVPN() + } else { + segueToPrivacyPro() + } + } + } + private func registerForKeyboardNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame), diff --git a/DuckDuckGo/NewAppDelegate.swift b/DuckDuckGo/NewAppDelegate.swift deleted file mode 100644 index 6d6adc6bc8..0000000000 --- a/DuckDuckGo/NewAppDelegate.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// NewAppDelegate.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import UIKit - -final class NewAppDelegate: NSObject, UIApplicationDelegate, DDGApp { - - private let appStateMachine: AppStateMachine = AppStateMachine() - private let isTesting: Bool = ProcessInfo().arguments.contains("testing") - - var privacyProDataReporter: PrivacyProDataReporting? { - (appStateMachine.currentState as? Active)?.appDependencies.privacyProDataReporter // just for now, we have to get rid of this anti pattern - } - - func initialize() { } // init code will happen inside AppStateMachine/Init state .init() - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - appStateMachine.handle(.launching(application, isTesting: isTesting)) - return true - } - - func applicationDidBecomeActive(_ application: UIApplication) { - appStateMachine.handle(.activating) - } - - func applicationWillResignActive(_ application: UIApplication) { - appStateMachine.handle(.suspending) - } - - func applicationDidEnterBackground(_ application: UIApplication) { - appStateMachine.handle(.backgrounding) - } - - func application(_ application: UIApplication, - performActionFor shortcutItem: UIApplicationShortcutItem, - completionHandler: @escaping (Bool) -> Void) { - appStateMachine.handle(.handleShortcutItem(shortcutItem)) - } - - func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { - appStateMachine.handle(.openURL(url)) - return true - } - - func refreshRemoteMessages() { - // part of debug menu, let's not support it in the first iteration - } - - -} diff --git a/DuckDuckGo/OldAppDelegate.swift b/DuckDuckGo/OldAppDelegate.swift deleted file mode 100644 index 0b142a6907..0000000000 --- a/DuckDuckGo/OldAppDelegate.swift +++ /dev/null @@ -1,1265 +0,0 @@ -// -// OldAppDelegate.swift -// DuckDuckGo -// -// Copyright © 2017 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import UIKit -import Combine -import Common -import Core -import UserNotifications -import Kingfisher -import WidgetKit -import BackgroundTasks -import BrowserServicesKit -import Bookmarks -import Persistence -import Crashes -import Configuration -import Networking -import DDGSync -import RemoteMessaging -import SyncDataProviders -import Subscription -import NetworkProtection -import PixelKit -import PixelExperimentKit -import WebKit -import os.log - -@MainActor -final class OldAppDelegate: NSObject, UIApplicationDelegate, DDGApp { - - private var testing = false - var appIsLaunching = false - var overlayWindow: UIWindow? - var window: UIWindow? { - get { - appDelegate?.window - } - set { - appDelegate?.window = newValue - } - } - - private lazy var privacyStore = PrivacyUserDefaults() - private var bookmarksDatabase: CoreDataDatabase = BookmarksDatabase.make() - - private let widgetRefreshModel = NetworkProtectionWidgetRefreshModel() - private let tunnelDefaults = UserDefaults.networkProtectionGroupDefaults - - @MainActor - private lazy var vpnWorkaround: VPNRedditSessionWorkaround = { - return VPNRedditSessionWorkaround( - accountManager: AppDependencyProvider.shared.accountManager, - tunnelController: AppDependencyProvider.shared.networkProtectionTunnelController - ) - }() - - private var autoClear: AutoClear? - private var showKeyboardIfSettingOn = true - private var lastBackgroundDate: Date? - - private(set) var homePageConfiguration: HomePageConfiguration! - - private(set) var remoteMessagingClient: RemoteMessagingClient! - - private(set) var syncService: DDGSync! - private(set) var syncDataProviders: SyncDataProviders! - private var syncDidFinishCancellable: AnyCancellable? - private var syncStateCancellable: AnyCancellable? - private var isSyncInProgressCancellable: AnyCancellable? - - private let crashCollection = CrashCollection(crashReportSender: CrashReportSender(platform: .iOS, - pixelEvents: CrashReportSender.pixelEvents), - crashCollectionStorage: UserDefaults()) - private var crashReportUploaderOnboarding: CrashCollectionOnboarding? - - private var autofillPixelReporter: AutofillPixelReporter? - private var autofillUsageMonitor = AutofillUsageMonitor() - - private(set) var subscriptionFeatureAvailability: SubscriptionFeatureAvailability! - private var subscriptionCookieManager: SubscriptionCookieManaging! - private var subscriptionCookieManagerFeatureFlagCancellable: AnyCancellable? - var privacyProDataReporter: PrivacyProDataReporting? - - // MARK: - Feature specific app event handlers - - private let tipKitAppEventsHandler = TipKitAppEventHandler() - - // MARK: lifecycle - - @UserDefaultsWrapper(key: .privacyConfigCustomURL, defaultValue: nil) - private var privacyConfigCustomURL: String? - - var accountManager: AccountManager { - AppDependencyProvider.shared.accountManager - } - - @UserDefaultsWrapper(key: .didCrashDuringCrashHandlersSetUp, defaultValue: false) - private var didCrashDuringCrashHandlersSetUp: Bool - - private let launchOptionsHandler = LaunchOptionsHandler() - private let onboardingPixelReporter = OnboardingPixelReporter() - - private let voiceSearchHelper = VoiceSearchHelper() - - private let marketplaceAdPostbackManager = MarketplaceAdPostbackManager() - - private var didFinishLaunchingStartTime: CFAbsoluteTime? - - private weak var appDelegate: AppDelegate? - init(with appDelegate: AppDelegate) { - self.appDelegate = appDelegate - } - - func initialize() { - if !didCrashDuringCrashHandlersSetUp { - didCrashDuringCrashHandlersSetUp = true - CrashLogMessageExtractor.setUp(swapCxaThrow: false) - didCrashDuringCrashHandlersSetUp = false - } - } - - // swiftlint:disable:next function_body_length - // swiftlint:disable:next cyclomatic_complexity - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - didFinishLaunchingStartTime = CFAbsoluteTimeGetCurrent() - defer { - if let didFinishLaunchingStartTime { - let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime - Pixel.fire(pixel: .appDidFinishLaunchingTime(time: Pixel.Event.BucketAggregation(number: launchTime)), - withAdditionalParameters: [PixelParameters.time: String(launchTime)]) - } - } - - -#if targetEnvironment(simulator) - if ProcessInfo.processInfo.environment["UITESTING"] == "true" { - // Disable hardware keyboards. - let setHardwareLayout = NSSelectorFromString("setHardwareLayout:") - UITextInputMode.activeInputModes - // Filter `UIKeyboardInputMode`s. - .filter({ $0.responds(to: setHardwareLayout) }) - .forEach { $0.perform(setHardwareLayout, with: nil) } - } -#endif - -#if DEBUG - Pixel.isDryRun = true -#else - Pixel.isDryRun = false -#endif - - ContentBlocking.shared.onCriticalError = presentPreemptiveCrashAlert - // Explicitly prepare ContentBlockingUpdating instance before Tabs are created - _ = ContentBlockingUpdating.shared - - // Can be removed after a couple of versions - cleanUpMacPromoExperiment2() - cleanUpIncrementalRolloutPixelTest() - - APIRequest.Headers.setUserAgent(DefaultUserAgentManager.duckDuckGoUserAgent) - - if isDebugBuild, let privacyConfigCustomURL, let url = URL(string: privacyConfigCustomURL) { - Configuration.setURLProvider(CustomConfigurationURLProvider(customPrivacyConfigurationURL: url)) - } else { - Configuration.setURLProvider(AppConfigurationURLProvider()) - } - - crashCollection.startAttachingCrashLogMessages { pixelParameters, payloads, sendReport in - pixelParameters.forEach { params in - Pixel.fire(pixel: .dbCrashDetected, withAdditionalParameters: params, includedParameters: []) - - // Each crash comes with an `appVersion` parameter representing the version that the crash occurred on. - // This is to disambiguate the situation where a crash occurs, but isn't sent until the next update. - // If for some reason the parameter can't be found, fall back to the current version. - if let crashAppVersion = params[PixelParameters.appVersion] { - let dailyParameters = [PixelParameters.appVersion: crashAppVersion] - DailyPixel.fireDaily(.dbCrashDetectedDaily, withAdditionalParameters: dailyParameters) - } else { - DailyPixel.fireDaily(.dbCrashDetectedDaily) - } - } - - // Async dispatch because rootViewController may otherwise be nil here - DispatchQueue.main.async { - guard let viewController = self.window?.rootViewController else { return } - - let crashReportUploaderOnboarding = CrashCollectionOnboarding(appSettings: AppDependencyProvider.shared.appSettings) - crashReportUploaderOnboarding.presentOnboardingIfNeeded(for: payloads, from: viewController, sendReport: sendReport) - self.crashReportUploaderOnboarding = crashReportUploaderOnboarding - } - } - - clearTmp() - - _ = DefaultUserAgentManager.shared - testing = ProcessInfo().arguments.contains("testing") - if testing { - Pixel.isDryRun = true - _ = DefaultUserAgentManager.shared - Database.shared.loadStore { _, _ in } - _ = BookmarksDatabaseSetup().loadStoreAndMigrate(bookmarksDatabase: bookmarksDatabase) - - window = UIWindow(frame: UIScreen.main.bounds) - window?.rootViewController = UIStoryboard.init(name: "LaunchScreen", bundle: nil).instantiateInitialViewController() - - let blockingDelegate = BlockingNavigationDelegate() - let webView = blockingDelegate.prepareWebView() - window?.rootViewController?.view.addSubview(webView) - window?.rootViewController?.view.backgroundColor = .red - webView.frame = CGRect(x: 10, y: 10, width: 300, height: 300) - - let request = URLRequest(url: URL(string: "about:blank")!) - webView.load(request) - - return true - } - - removeEmailWaitlistState() - - var shouldPresentInsufficientDiskSpaceAlertAndCrash = false - Database.shared.loadStore { context, error in - guard let context = context else { - - let parameters = [PixelParameters.applicationState: "\(application.applicationState.rawValue)", - PixelParameters.dataAvailability: "\(application.isProtectedDataAvailable)"] - - switch error { - case .none: - fatalError("Could not create database stack: Unknown Error") - case .some(CoreDataDatabase.Error.containerLocationCouldNotBePrepared(let underlyingError)): - Pixel.fire(pixel: .dbContainerInitializationError, - error: underlyingError, - withAdditionalParameters: parameters) - Thread.sleep(forTimeInterval: 1) - fatalError("Could not create database stack: \(underlyingError.localizedDescription)") - case .some(let error): - Pixel.fire(pixel: .dbInitializationError, - error: error, - withAdditionalParameters: parameters) - if error.isDiskFull { - shouldPresentInsufficientDiskSpaceAlertAndCrash = true - return - } else { - Thread.sleep(forTimeInterval: 1) - fatalError("Could not create database stack: \(error.localizedDescription)") - } - } - } - DatabaseMigration.migrate(to: context) - } - - switch BookmarksDatabaseSetup().loadStoreAndMigrate(bookmarksDatabase: bookmarksDatabase) { - case .success: - break - case .failure(let error): - Pixel.fire(pixel: .bookmarksCouldNotLoadDatabase, - error: error) - if error.isDiskFull { - shouldPresentInsufficientDiskSpaceAlertAndCrash = true - } else { - Thread.sleep(forTimeInterval: 1) - fatalError("Could not create database stack: \(error.localizedDescription)") - } - } - - WidgetCenter.shared.reloadAllTimelines() - - Favicons.shared.migrateFavicons(to: Favicons.Constants.maxFaviconSize) { - WidgetCenter.shared.reloadAllTimelines() - } - - PrivacyFeatures.httpsUpgrade.loadDataAsync() - - let variantManager = DefaultVariantManager() - let daxDialogs = DaxDialogs.shared - - // assign it here, because "did become active" is already too late and "viewWillAppear" - // has already been called on the HomeViewController so won't show the home row CTA - cleanUpATBAndAssignVariant(variantManager: variantManager, daxDialogs: daxDialogs) - - // MARK: Sync initialisation -#if DEBUG - let defaultEnvironment = ServerEnvironment.development -#else - let defaultEnvironment = ServerEnvironment.production -#endif - - let environment = ServerEnvironment( - UserDefaultsWrapper( - key: .syncEnvironment, - defaultValue: defaultEnvironment.description - ).wrappedValue - ) ?? defaultEnvironment - - var dryRun = false -#if DEBUG - dryRun = true -#endif - let isPhone = UIDevice.current.userInterfaceIdiom == .phone - let source = isPhone ? PixelKit.Source.iOS : PixelKit.Source.iPadOS - PixelKit.setUp(dryRun: dryRun, - appVersion: AppVersion.shared.versionNumber, - source: source.rawValue, - defaultHeaders: [:], - defaults: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults()) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in - - let url = URL.pixelUrl(forPixelNamed: pixelName) - let apiHeaders = APIRequestV2.HeadersV2(additionalHeaders: headers) - let request = APIRequestV2(url: url, method: .get, queryItems: parameters, headers: apiHeaders) - Task { - do { - _ = try await DefaultAPIService().fetch(request: request) - onComplete(true, nil) - } catch { - onComplete(false, error) - } - } - } - PixelKit.configureExperimentKit(featureFlagger: AppDependencyProvider.shared.featureFlagger, - eventTracker: ExperimentEventTracker(store: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults())) - - let syncErrorHandler = SyncErrorHandler() - - syncDataProviders = SyncDataProviders( - bookmarksDatabase: bookmarksDatabase, - secureVaultErrorReporter: SecureVaultReporter(), - settingHandlers: [FavoritesDisplayModeSyncHandler()], - favoritesDisplayModeStorage: FavoritesDisplayModeStorage(), - syncErrorHandler: syncErrorHandler, - faviconStoring: Favicons.shared, - tld: AppDependencyProvider.shared.storageCache.tld - ) - - let syncService = DDGSync( - dataProvidersSource: syncDataProviders, - errorEvents: SyncErrorHandler(), - privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, - environment: environment - ) - syncService.initializeIfNeeded() - self.syncService = syncService - - let fireproofing = UserDefaultsFireproofing.xshared - privacyProDataReporter = PrivacyProDataReporter(fireproofing: fireproofing) - - isSyncInProgressCancellable = syncService.isSyncInProgressPublisher - .filter { $0 } - .sink { [weak syncService] _ in - DailyPixel.fire(pixel: .syncDaily, includedParameters: [.appVersion]) - syncService?.syncDailyStats.sendStatsIfNeeded(handler: { params in - Pixel.fire(pixel: .syncSuccessRateDaily, - withAdditionalParameters: params, - includedParameters: [.appVersion]) - }) - } - - remoteMessagingClient = RemoteMessagingClient( - bookmarksDatabase: bookmarksDatabase, - appSettings: AppDependencyProvider.shared.appSettings, - internalUserDecider: AppDependencyProvider.shared.internalUserDecider, - configurationStore: AppDependencyProvider.shared.configurationStore, - database: Database.shared, - errorEvents: RemoteMessagingStoreErrorHandling(), - remoteMessagingAvailabilityProvider: PrivacyConfigurationRemoteMessagingAvailabilityProvider( - privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager - ), - duckPlayerStorage: DefaultDuckPlayerStorage() - ) - remoteMessagingClient.registerBackgroundRefreshTaskHandler() - - subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability( - privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, - purchasePlatform: .appStore) - - subscriptionCookieManager = makeSubscriptionCookieManager() - - homePageConfiguration = HomePageConfiguration(variantManager: AppDependencyProvider.shared.variantManager, - remoteMessagingClient: remoteMessagingClient, - privacyProDataReporter: privacyProDataReporter!) - - let previewsSource = TabPreviewsSource() - let historyManager = makeHistoryManager() - let tabsModel = prepareTabsModel(previewsSource: previewsSource) - - privacyProDataReporter?.injectTabsModel(tabsModel) - - if shouldPresentInsufficientDiskSpaceAlertAndCrash { - - window = UIWindow(frame: UIScreen.main.bounds) - window?.rootViewController = BlankSnapshotViewController(addressBarPosition: AppDependencyProvider.shared.appSettings.currentAddressBarPosition, - voiceSearchHelper: voiceSearchHelper) - window?.makeKeyAndVisible() - - presentInsufficientDiskSpaceAlert() - } else { - let daxDialogsFactory = ExperimentContextualDaxDialogsFactory(contextualOnboardingLogic: daxDialogs, contextualOnboardingPixelReporter: onboardingPixelReporter) - let contextualOnboardingPresenter = ContextualOnboardingPresenter(variantManager: variantManager, daxDialogsFactory: daxDialogsFactory) - let main = MainViewController(bookmarksDatabase: bookmarksDatabase, - bookmarksDatabaseCleaner: syncDataProviders.bookmarksAdapter.databaseCleaner, - historyManager: historyManager, - homePageConfiguration: homePageConfiguration, - syncService: syncService, - syncDataProviders: syncDataProviders, - appSettings: AppDependencyProvider.shared.appSettings, - previewsSource: previewsSource, - tabsModel: tabsModel, - syncPausedStateManager: syncErrorHandler, - privacyProDataReporter: privacyProDataReporter!, - variantManager: variantManager, - contextualOnboardingPresenter: contextualOnboardingPresenter, - contextualOnboardingLogic: daxDialogs, - contextualOnboardingPixelReporter: onboardingPixelReporter, - subscriptionFeatureAvailability: subscriptionFeatureAvailability, - voiceSearchHelper: voiceSearchHelper, - featureFlagger: AppDependencyProvider.shared.featureFlagger, - fireproofing: fireproofing, - subscriptionCookieManager: subscriptionCookieManager, - textZoomCoordinator: makeTextZoomCoordinator(), - websiteDataManager: makeWebsiteDataManager(fireproofing: fireproofing), - appDidFinishLaunchingStartTime: didFinishLaunchingStartTime) - - main.loadViewIfNeeded() - syncErrorHandler.alertPresenter = main - - window = UIWindow(frame: UIScreen.main.bounds) - window?.rootViewController = main - window?.makeKeyAndVisible() - - autoClear = AutoClear(worker: main) - let applicationState = application.applicationState - Task { - await autoClear?.clearDataIfEnabled(applicationState: .init(with: applicationState)) - await vpnWorkaround.installRedditSessionWorkaround() - } - } - - self.voiceSearchHelper.migrateSettingsFlagIfNecessary() - - // Task handler registration needs to happen before the end of `didFinishLaunching`, otherwise submitting a task can throw an exception. - // Having both in `didBecomeActive` can sometimes cause the exception when running on a physical device, so registration happens here. - AppConfigurationFetch.registerBackgroundRefreshTaskHandler() - - UNUserNotificationCenter.current().delegate = self - - window?.windowScene?.screenshotService?.delegate = self - ThemeManager.shared.updateUserInterfaceStyle(window: window) - - appIsLaunching = true - - // Temporary logic for rollout of Autofill as on by default for new installs only - if AppDependencyProvider.shared.appSettings.autofillIsNewInstallForOnByDefault == nil { - AppDependencyProvider.shared.appSettings.setAutofillIsNewInstallForOnByDefault() - } - - NewTabPageIntroMessageSetup().perform() - - widgetRefreshModel.beginObservingVPNStatus() - - AppDependencyProvider.shared.subscriptionManager.loadInitialData() - - setUpAutofillPixelReporter() - - if didCrashDuringCrashHandlersSetUp { - Pixel.fire(pixel: .crashOnCrashHandlersSetUp) - didCrashDuringCrashHandlersSetUp = false - } - - tipKitAppEventsHandler.appDidFinishLaunching() - - return true - } - - private func makeWebsiteDataManager(fireproofing: Fireproofing, - dataStoreIDManager: DataStoreIDManaging = DataStoreIDManager.shared) -> WebsiteDataManaging { - return WebCacheManager(cookieStorage: MigratableCookieStorage(), - fireproofing: fireproofing, - dataStoreIDManager: dataStoreIDManager) - } - - private func makeTextZoomCoordinator() -> TextZoomCoordinator { - let provider = AppDependencyProvider.shared - let storage = TextZoomStorage() - - return TextZoomCoordinator(appSettings: provider.appSettings, - storage: storage, - featureFlagger: provider.featureFlagger) - } - - private func makeSubscriptionCookieManager() -> SubscriptionCookieManaging { - let subscriptionCookieManager = SubscriptionCookieManager(subscriptionManager: AppDependencyProvider.shared.subscriptionManager, - currentCookieStore: { [weak self] in - guard self?.mainViewController?.tabManager.model.hasActiveTabs ?? false else { - // We shouldn't interact with WebKit's cookie store unless we have a WebView, - // eventually the subscription cookie will be refreshed on opening the first tab - return nil - } - - return WKHTTPCookieStoreWrapper(store: WKWebsiteDataStore.current().httpCookieStore) - }, eventMapping: SubscriptionCookieManageEventPixelMapping()) - - - let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager - - // Enable subscriptionCookieManager if feature flag is present - if privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) { - subscriptionCookieManager.enableSettingSubscriptionCookie() - } - - // Keep track of feature flag changes - subscriptionCookieManagerFeatureFlagCancellable = privacyConfigurationManager.updatesPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self, weak privacyConfigurationManager] in - guard let self, !self.appIsLaunching, let privacyConfigurationManager else { return } - - let isEnabled = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) - - Task { @MainActor [weak self] in - if isEnabled { - self?.subscriptionCookieManager.enableSettingSubscriptionCookie() - } else { - await self?.subscriptionCookieManager.disableSettingSubscriptionCookie() - } - } - } - - return subscriptionCookieManager - } - - private func makeHistoryManager() -> HistoryManaging { - - let provider = AppDependencyProvider.shared - - switch HistoryManager.make(isAutocompleteEnabledByUser: provider.appSettings.autocomplete, - isRecentlyVisitedSitesEnabledByUser: provider.appSettings.recentlyVisitedSites, - privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager, - tld: provider.storageCache.tld) { - - case .failure(let error): - Pixel.fire(pixel: .historyStoreLoadFailed, error: error) - if error.isDiskFull { - self.presentInsufficientDiskSpaceAlert() - } else { - self.presentPreemptiveCrashAlert() - } - return NullHistoryManager() - - case .success(let historyManager): - return historyManager - } - } - - private func prepareTabsModel(previewsSource: TabPreviewsSource = TabPreviewsSource(), - appSettings: AppSettings = AppDependencyProvider.shared.appSettings, - isDesktop: Bool = UIDevice.current.userInterfaceIdiom == .pad) -> TabsModel { - let isPadDevice = UIDevice.current.userInterfaceIdiom == .pad - let tabsModel: TabsModel - if AutoClearSettingsModel(settings: appSettings) != nil { - tabsModel = TabsModel(desktop: isPadDevice) - tabsModel.save() - previewsSource.removeAllPreviews() - } else { - if let storedModel = TabsModel.get() { - // Save new model in case of migration - storedModel.save() - tabsModel = storedModel - } else { - tabsModel = TabsModel(desktop: isPadDevice) - } - } - return tabsModel - } - - private func presentPreemptiveCrashAlert() { - Task { @MainActor in - let alertController = CriticalAlerts.makePreemptiveCrashAlert() - window?.rootViewController?.present(alertController, animated: true, completion: nil) - } - } - - private func presentInsufficientDiskSpaceAlert() { - let alertController = CriticalAlerts.makeInsufficientDiskSpaceAlert() - window?.rootViewController?.present(alertController, animated: true, completion: nil) - } - - private func presentExpiredEntitlementAlert() { - let alertController = CriticalAlerts.makeExpiredEntitlementAlert { [weak self] in - self?.mainViewController?.segueToPrivacyPro() - } - window?.rootViewController?.present(alertController, animated: true) { [weak self] in - self?.tunnelDefaults.showEntitlementAlert = false - } - } - - private func presentExpiredEntitlementNotificationIfNeeded() { - let presenter = NetworkProtectionNotificationsPresenterTogglableDecorator( - settings: AppDependencyProvider.shared.vpnSettings, - defaults: .networkProtectionGroupDefaults, - wrappee: NetworkProtectionUNNotificationPresenter() - ) - presenter.showEntitlementNotification() - } - - private func cleanUpMacPromoExperiment2() { - UserDefaults.standard.removeObject(forKey: "com.duckduckgo.ios.macPromoMay23.exp2.cohort") - } - - private func cleanUpIncrementalRolloutPixelTest() { - UserDefaults.standard.removeObject(forKey: "network-protection.incremental-feature-flag-test.has-sent-pixel") - } - - private func clearTmp() { - let tmp = FileManager.default.temporaryDirectory - do { - try FileManager.default.removeItem(at: tmp) - } catch { - Logger.general.error("Failed to delete tmp dir") - } - } - - private func reportAdAttribution() { - Task.detached(priority: .background) { - await AdAttributionPixelReporter.shared.reportAttributionIfNeeded() - } - } - - func applicationDidBecomeActive(_ application: UIApplication) { - guard !testing else { return } - - defer { - if let didFinishLaunchingStartTime { - let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime - Pixel.fire(pixel: .appDidBecomeActiveTime(time: Pixel.Event.BucketAggregation(number: launchTime)), - withAdditionalParameters: [PixelParameters.time: String(launchTime)]) - } - } - - StorageInconsistencyMonitor().didBecomeActive(isProtectedDataAvailable: application.isProtectedDataAvailable) - syncService.initializeIfNeeded() - syncDataProviders.setUpDatabaseCleanersIfNeeded(syncService: syncService) - - if !(overlayWindow?.rootViewController is AuthenticationViewController) { - removeOverlay() - } - - StatisticsLoader.shared.load { - StatisticsLoader.shared.refreshAppRetentionAtb() - self.fireAppLaunchPixel() - self.reportAdAttribution() - self.onboardingPixelReporter.fireEnqueuedPixelsIfNeeded() - } - - if appIsLaunching { - appIsLaunching = false - onApplicationLaunch(application) - } - - mainViewController?.showBars() - mainViewController?.didReturnFromBackground() - - if !privacyStore.authenticationEnabled { - showKeyboardOnLaunch() - } - - if AppConfigurationFetch.shouldScheduleRulesCompilationOnAppLaunch { - ContentBlocking.shared.contentBlockingManager.scheduleCompilation() - AppConfigurationFetch.shouldScheduleRulesCompilationOnAppLaunch = false - } - AppDependencyProvider.shared.configurationManager.loadPrivacyConfigFromDiskIfNeeded() - - AppConfigurationFetch().start { result in - self.sendAppLaunchPostback() - if case .assetsUpdated(let protectionsUpdated) = result, protectionsUpdated { - ContentBlocking.shared.contentBlockingManager.scheduleCompilation() - } - } - - syncService.scheduler.notifyAppLifecycleEvent() - - privacyProDataReporter?.injectSyncService(syncService) - - fireFailedCompilationsPixelIfNeeded() - - widgetRefreshModel.refreshVPNWidget() - - if tunnelDefaults.showEntitlementAlert { - presentExpiredEntitlementAlert() - } - - presentExpiredEntitlementNotificationIfNeeded() - - Task { - await stopAndRemoveVPNIfNotAuthenticated() - await refreshShortcuts() - await vpnWorkaround.installRedditSessionWorkaround() - - if #available(iOS 17.0, *) { - await VPNSnoozeLiveActivityManager().endSnoozeActivityIfNecessary() - } - } - - AppDependencyProvider.shared.subscriptionManager.refreshCachedSubscriptionAndEntitlements { isSubscriptionActive in - if isSubscriptionActive { - DailyPixel.fire(pixel: .privacyProSubscriptionActive) - } - } - - Task { - await subscriptionCookieManager.refreshSubscriptionCookie() - } - - let importPasswordsStatusHandler = ImportPasswordsStatusHandler(syncService: syncService) - importPasswordsStatusHandler.checkSyncSuccessStatus() - - Task { - await privacyProDataReporter?.saveWidgetAdded() - } - - AppDependencyProvider.shared.persistentPixel.sendQueuedPixels { _ in } - } - - private func stopAndRemoveVPNIfNotAuthenticated() async { - // Only remove the VPN if the user is not authenticated, and it's installed: - guard !accountManager.isUserAuthenticated, await AppDependencyProvider.shared.networkProtectionTunnelController.isInstalled else { - return - } - - await AppDependencyProvider.shared.networkProtectionTunnelController.stop() - await AppDependencyProvider.shared.networkProtectionTunnelController.removeVPN(reason: .didBecomeActiveCheck) - } - - func applicationWillResignActive(_ application: UIApplication) { - Task { @MainActor in - await refreshShortcuts() - await vpnWorkaround.removeRedditSessionWorkaround() - } - } - - private func fireAppLaunchPixel() { - - WidgetCenter.shared.getCurrentConfigurations { result in - let paramKeys: [WidgetFamily: String] = [ - .systemSmall: PixelParameters.widgetSmall, - .systemMedium: PixelParameters.widgetMedium, - .systemLarge: PixelParameters.widgetLarge - ] - - switch result { - case .failure(let error): - Pixel.fire(pixel: .appLaunch, withAdditionalParameters: [ - PixelParameters.widgetError: "1", - PixelParameters.widgetErrorCode: "\((error as NSError).code)", - PixelParameters.widgetErrorDomain: (error as NSError).domain - ], includedParameters: [.appVersion, .atb]) - - case .success(let widgetInfo): - let params = widgetInfo.reduce([String: String]()) { - var result = $0 - if let key = paramKeys[$1.family] { - result[key] = "1" - } - return result - } - Pixel.fire(pixel: .appLaunch, withAdditionalParameters: params, includedParameters: [.appVersion, .atb]) - } - - } - } - - private func fireFailedCompilationsPixelIfNeeded() { - let store = FailedCompilationsStore() - if store.hasAnyFailures { - DailyPixel.fire(pixel: .compilationFailed, withAdditionalParameters: store.summary) { error in - guard error != nil else { return } - store.cleanup() - } - } - } - - private func shouldShowKeyboardOnLaunch() -> Bool { - guard let date = lastBackgroundDate else { return true } - return Date().timeIntervalSince(date) > AppDelegate.ShowKeyboardOnLaunchThreshold - } - - private func showKeyboardOnLaunch() { - guard KeyboardSettings().onAppLaunch && showKeyboardIfSettingOn && shouldShowKeyboardOnLaunch() else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.mainViewController?.enterSearch() - } - showKeyboardIfSettingOn = false - } - - private func onApplicationLaunch(_ application: UIApplication) { - Task { @MainActor in - await beginAuthentication() - initialiseBackgroundFetch(application) - applyAppearanceChanges() - refreshRemoteMessages() - } - } - - private func applyAppearanceChanges() { - UILabel.appearance(whenContainedInInstancesOf: [UIAlertController.self]).numberOfLines = 0 - } - - /// It's public in order to allow refreshing on demand via Debug menu. Otherwise it shouldn't be called from outside. - func refreshRemoteMessages() { - Task { - try? await remoteMessagingClient.fetchAndProcess(using: remoteMessagingClient.store) - } - } - - func applicationWillEnterForeground(_ application: UIApplication) { - ThemeManager.shared.updateUserInterfaceStyle() - - Task { @MainActor in - await beginAuthentication() - await autoClear?.clearDataIfEnabledAndTimeExpired(applicationState: .active) - showKeyboardIfSettingOn = true - syncService.scheduler.resumeSyncQueue() - } - } - - func applicationDidEnterBackground(_ application: UIApplication) { - displayBlankSnapshotWindow() - autoClear?.startClearingTimer() - lastBackgroundDate = Date() - AppDependencyProvider.shared.autofillLoginSession.endSession() - suspendSync() - syncDataProviders.bookmarksAdapter.cancelFaviconsFetching(application) - privacyProDataReporter?.saveApplicationLastSessionEnded() - resetAppStartTime() - } - - private func resetAppStartTime() { - didFinishLaunchingStartTime = nil - mainViewController?.appDidFinishLaunchingStartTime = nil - } - - private func suspendSync() { - if syncService.isSyncInProgress { - Logger.sync.debug("Sync is in progress. Starting background task to allow it to gracefully complete.") - - var taskID: UIBackgroundTaskIdentifier! - taskID = UIApplication.shared.beginBackgroundTask(withName: "Cancelled Sync Completion Task") { - Logger.sync.debug("Forcing background task completion") - UIApplication.shared.endBackgroundTask(taskID) - } - syncDidFinishCancellable?.cancel() - syncDidFinishCancellable = syncService.isSyncInProgressPublisher.filter { !$0 } - .prefix(1) - .receive(on: DispatchQueue.main) - .sink { _ in - Logger.sync.debug("Ending background task") - UIApplication.shared.endBackgroundTask(taskID) - } - } - - syncService.scheduler.cancelSyncAndSuspendSyncQueue() - } - - func application(_ application: UIApplication, - performActionFor shortcutItem: UIApplicationShortcutItem, - completionHandler: @escaping (Bool) -> Void) { - handleShortCutItem(shortcutItem) - } - - func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { - Logger.sync.debug("App launched with url \(url.absoluteString)") - - // If showing the onboarding intro ignore deeplinks - guard mainViewController?.needsToShowOnboardingIntro() == false else { - return false - } - - if handleEmailSignUpDeepLink(url) { - return true - } - - NotificationCenter.default.post(name: AutofillLoginListAuthenticator.Notifications.invalidateContext, object: nil) - - // The openVPN action handles the navigation stack on its own and does not need it to be cleared - if url != AppDeepLinkSchemes.openVPN.url { - mainViewController?.clearNavigationStack() - } - - Task { @MainActor in - // Autoclear should have happened by now - showKeyboardIfSettingOn = false - - if !handleAppDeepLink(app, mainViewController, url) { - mainViewController?.loadUrlInNewTab(url, reuseExisting: true, inheritedAttribution: nil, fromExternalLink: true) - } - } - - return true - } - - func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - - Logger.lifecycle.debug(#function) - - AppConfigurationFetch().start(isBackgroundFetch: true) { result in - switch result { - case .noData: - completionHandler(.noData) - case .assetsUpdated: - completionHandler(.newData) - } - } - } - - func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool { - return true - } - - // MARK: private - - private func sendAppLaunchPostback() { - // Attribution support - let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager - if privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .marketplaceAdPostback) { - marketplaceAdPostbackManager.sendAppLaunchPostback() - } - } - - private func cleanUpATBAndAssignVariant(variantManager: VariantManager, daxDialogs: DaxDialogs) { - let historyMessageManager = HistoryMessageManager() - - AtbAndVariantCleanup.cleanup() - variantManager.assignVariantIfNeeded { _ in - // MARK: perform first time launch logic here - // If it's running UI Tests check if the onboarding should be in a completed state. - if launchOptionsHandler.isUITesting && launchOptionsHandler.isOnboardingCompleted { - daxDialogs.dismiss() - } else { - daxDialogs.primeForUse() - } - - // New users don't see the message - historyMessageManager.dismiss() - - // Setup storage for marketplace postback - marketplaceAdPostbackManager.updateReturningUserValue() - } - } - - private func initialiseBackgroundFetch(_ application: UIApplication) { - guard UIApplication.shared.backgroundRefreshStatus == .available else { - return - } - - // BackgroundTasks will automatically replace an existing task in the queue if one with the same identifier is queued, so we should only - // schedule a task if there are none pending in order to avoid the config task getting perpetually replaced. - BGTaskScheduler.shared.getPendingTaskRequests { tasks in - let hasConfigurationTask = tasks.contains { $0.identifier == AppConfigurationFetch.Constants.backgroundProcessingTaskIdentifier } - if !hasConfigurationTask { - AppConfigurationFetch.scheduleBackgroundRefreshTask() - } - - let hasRemoteMessageFetchTask = tasks.contains { $0.identifier == RemoteMessagingClient.Constants.backgroundRefreshTaskIdentifier } - if !hasRemoteMessageFetchTask { - RemoteMessagingClient.scheduleBackgroundRefreshTask() - } - } - } - - private func displayAuthenticationWindow() { - guard overlayWindow == nil, let frame = window?.frame else { return } - overlayWindow = UIWindow(frame: frame) - overlayWindow?.windowLevel = UIWindow.Level.alert - overlayWindow?.rootViewController = AuthenticationViewController.loadFromStoryboard() - overlayWindow?.makeKeyAndVisible() - window?.isHidden = true - } - - private func displayBlankSnapshotWindow() { - guard overlayWindow == nil, let frame = window?.frame else { return } - guard autoClear?.isClearingEnabled ?? false || privacyStore.authenticationEnabled else { return } - - overlayWindow = UIWindow(frame: frame) - overlayWindow?.windowLevel = UIWindow.Level.alert - - let overlay = BlankSnapshotViewController(addressBarPosition: AppDependencyProvider.shared.appSettings.currentAddressBarPosition, - voiceSearchHelper: voiceSearchHelper) - overlay.delegate = self - - overlayWindow?.rootViewController = overlay - overlayWindow?.makeKeyAndVisible() - window?.isHidden = true - } - - private func beginAuthentication() async { - - guard privacyStore.authenticationEnabled else { return } - - removeOverlay() - displayAuthenticationWindow() - - guard let controller = overlayWindow?.rootViewController as? AuthenticationViewController else { - removeOverlay() - return - } - - await controller.beginAuthentication { [weak self] in - self?.removeOverlay() - self?.showKeyboardOnLaunch() - } - } - - private func tryToObtainOverlayWindow() { - for window in UIApplication.shared.foregroundSceneWindows where window.rootViewController is BlankSnapshotViewController { - overlayWindow = window - return - } - } - - private func removeOverlay() { - if overlayWindow == nil { - tryToObtainOverlayWindow() - } - - if let overlay = overlayWindow { - overlay.isHidden = true - overlayWindow = nil - window?.makeKeyAndVisible() - } - } - - private func handleShortCutItem(_ shortcutItem: UIApplicationShortcutItem) { - Logger.general.debug("Handling shortcut item: \(shortcutItem.type)") - - Task { @MainActor in - - if appIsLaunching { - await autoClear?.clearDataIfEnabled() - } else { - await autoClear?.clearDataIfEnabledAndTimeExpired(applicationState: .active) - } - - if shortcutItem.type == AppDelegate.ShortcutKey.clipboard, let query = UIPasteboard.general.string { - mainViewController?.clearNavigationStack() - mainViewController?.loadQueryInNewTab(query) - return - } - - if shortcutItem.type == AppDelegate.ShortcutKey.passwords { - mainViewController?.clearNavigationStack() - // Give the `clearNavigationStack` call time to complete. - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) { [weak self] in - self?.mainViewController?.launchAutofillLogins(openSearch: true, source: .appIconShortcut) - } - Pixel.fire(pixel: .autofillLoginsLaunchAppShortcut) - return - } - - if shortcutItem.type == AppDelegate.ShortcutKey.openVPNSettings { - presentNetworkProtectionStatusSettingsModal() - } - - } - } - - private func removeEmailWaitlistState() { - EmailWaitlist.removeEmailState() - - let autofillStorage = EmailKeychainManager() - try? autofillStorage.deleteWaitlistState() - - // Remove the authentication state if this is a fresh install. - if !Database.shared.isDatabaseFileInitialized { - try? autofillStorage.deleteAuthenticationState() - } - } - - private func handleEmailSignUpDeepLink(_ url: URL) -> Bool { - guard url.absoluteString.starts(with: URL.emailProtection.absoluteString), - let navViewController = mainViewController?.presentedViewController as? UINavigationController, - let emailSignUpViewController = navViewController.topViewController as? EmailSignupViewController else { - return false - } - emailSignUpViewController.loadUrl(url) - return true - } - - private var mainViewController: MainViewController? { - return window?.rootViewController as? MainViewController - } - - private func setUpAutofillPixelReporter() { - autofillPixelReporter = AutofillPixelReporter( - userDefaults: .standard, - autofillEnabled: AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled, - eventMapping: EventMapping {[weak self] event, _, params, _ in - switch event { - case .autofillActiveUser: - Pixel.fire(pixel: .autofillActiveUser) - case .autofillEnabledUser: - Pixel.fire(pixel: .autofillEnabledUser) - case .autofillOnboardedUser: - Pixel.fire(pixel: .autofillOnboardedUser) - case .autofillToggledOn: - Pixel.fire(pixel: .autofillToggledOn, withAdditionalParameters: params ?? [:]) - if let autofillExtensionToggled = self?.autofillUsageMonitor.autofillExtensionEnabled { - Pixel.fire(pixel: autofillExtensionToggled ? .autofillExtensionToggledOn : .autofillExtensionToggledOff, - withAdditionalParameters: params ?? [:]) - } - case .autofillToggledOff: - Pixel.fire(pixel: .autofillToggledOff, withAdditionalParameters: params ?? [:]) - if let autofillExtensionToggled = self?.autofillUsageMonitor.autofillExtensionEnabled { - Pixel.fire(pixel: autofillExtensionToggled ? .autofillExtensionToggledOn : .autofillExtensionToggledOff, - withAdditionalParameters: params ?? [:]) - } - case .autofillLoginsStacked: - Pixel.fire(pixel: .autofillLoginsStacked, withAdditionalParameters: params ?? [:]) - default: - break - } - }, - installDate: StatisticsUserDefaults().installDate ?? Date()) - - _ = NotificationCenter.default.addObserver(forName: AppUserDefaults.Notifications.autofillEnabledChange, - object: nil, - queue: nil) { [weak self] _ in - self?.autofillPixelReporter?.updateAutofillEnabledStatus(AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled) - } - } - - @MainActor - func refreshShortcuts() async { - guard AppDependencyProvider.shared.vpnFeatureVisibility.shouldShowVPNShortcut() else { - UIApplication.shared.shortcutItems = nil - return - } - - if case .success(true) = await accountManager.hasEntitlement(forProductName: .networkProtection, cachePolicy: .returnCacheDataDontLoad) { - let items = [ - UIApplicationShortcutItem(type: AppDelegate.ShortcutKey.openVPNSettings, - localizedTitle: UserText.netPOpenVPNQuickAction, - localizedSubtitle: nil, - icon: UIApplicationShortcutIcon(templateImageName: "VPN-16"), - userInfo: nil) - ] - - UIApplication.shared.shortcutItems = items - } else { - UIApplication.shared.shortcutItems = nil - } - } -} - - -extension OldAppDelegate: BlankSnapshotViewRecoveringDelegate { - - func recoverFromPresenting(controller: BlankSnapshotViewController) { - if overlayWindow == nil { - tryToObtainOverlayWindow() - } - - overlayWindow?.isHidden = true - overlayWindow = nil - window?.makeKeyAndVisible() - } - -} - -extension OldAppDelegate: UIScreenshotServiceDelegate { - func screenshotService(_ screenshotService: UIScreenshotService, - generatePDFRepresentationWithCompletion completionHandler: @escaping (Data?, Int, CGRect) -> Void) { - guard let webView = mainViewController?.currentTab?.webView else { - completionHandler(nil, 0, .zero) - return - } - - let zoomScale = webView.scrollView.zoomScale - - // The PDF's coordinate space has its origin at the bottom left, so the view's origin.y needs to be converted - let visibleBounds = CGRect( - x: webView.scrollView.contentOffset.x / zoomScale, - y: (webView.scrollView.contentSize.height - webView.scrollView.contentOffset.y - webView.bounds.height) / zoomScale, - width: webView.bounds.width / zoomScale, - height: webView.bounds.height / zoomScale - ) - - webView.createPDF { result in - let data = try? result.get() - completionHandler(data, 0, visibleBounds) - } - } -} - -extension OldAppDelegate: UNUserNotificationCenterDelegate { - - func userNotificationCenter(_ center: UNUserNotificationCenter, - willPresent notification: UNNotification, - withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - completionHandler(.banner) - } - - func userNotificationCenter(_ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void) { - if response.actionIdentifier == UNNotificationDefaultActionIdentifier { - let identifier = response.notification.request.identifier - - if NetworkProtectionNotificationIdentifier(rawValue: identifier) != nil { - presentNetworkProtectionStatusSettingsModal() - } - } - - completionHandler() - } - - func presentNetworkProtectionStatusSettingsModal() { - Task { - if case .success(let hasEntitlements) = await accountManager.hasEntitlement(forProductName: .networkProtection), hasEntitlements { - (window?.rootViewController as? MainViewController)?.segueToVPN() - } else { - (window?.rootViewController as? MainViewController)?.segueToPrivacyPro() - } - } - } - - private func presentSettings(with viewController: UIViewController) { - guard let window = window, let rootViewController = window.rootViewController as? MainViewController else { return } - - if let navigationController = rootViewController.presentedViewController as? UINavigationController { - if let lastViewController = navigationController.viewControllers.last, lastViewController.isKind(of: type(of: viewController)) { - // Avoid presenting dismissing and re-presenting the view controller if it's already visible: - return - } else { - // Otherwise, replace existing view controllers with the presented one: - navigationController.popToRootViewController(animated: false) - navigationController.pushViewController(viewController, animated: false) - return - } - } - - // If the previous checks failed, make sure the nav stack is reset and present the view controller from scratch: - rootViewController.clearNavigationStack() - - // Give the `clearNavigationStack` call time to complete. - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) { - rootViewController.segueToSettings() - let navigationController = rootViewController.presentedViewController as? UINavigationController - navigationController?.popToRootViewController(animated: false) - navigationController?.pushViewController(viewController, animated: false) - } - } -} From b3ed3956149ecd6340504463cbf60e4cde4d678a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Wed, 15 Jan 2025 15:38:20 +0100 Subject: [PATCH 2/8] Extract open url and shortcut item handling from app lifecycle events --- Core/PixelEvent.swift | 4 +- DuckDuckGo/AppLifecycle/AppStateMachine.swift | 14 ++++++- .../AppLifecycle/AppStateTransitions.swift | 40 ++----------------- .../AppLifecycle/AppStates/Background.swift | 13 ++++++ .../AppLifecycle/AppStates/Foreground.swift | 13 ++++++ .../AppLifecycle/AppStates/Initializing.swift | 7 ++++ .../AppLifecycle/AppStates/Launching.swift | 13 ++++++ .../AppLifecycle/AppStates/Resuming.swift | 14 +++++++ .../AppLifecycle/AppStates/Suspending.swift | 13 ++++++ .../AppLifecycle/AppStates/Testing.swift | 6 +++ 10 files changed, 96 insertions(+), 41 deletions(-) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index fe7fbcd154..80790793ba 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -974,7 +974,6 @@ extension Pixel { // MARK: Lifecycle case appDidTransitionToUnexpectedState - case appDidConsecutivelyBackground } @@ -1946,8 +1945,7 @@ extension Pixel.Event { case .openAIChatFromAddressBar: return "m_aichat_addressbar_icon" // MARK: Lifecycle - case .appDidTransitionToUnexpectedState: return "m_debug_app-did-transition-to-unexpected-state-2" - case .appDidConsecutivelyBackground: return "m_debug_app-did-consecutively-background-2" + case .appDidTransitionToUnexpectedState: return "m_debug_app-did-transition-to-unexpected-state-3" } } diff --git a/DuckDuckGo/AppLifecycle/AppStateMachine.swift b/DuckDuckGo/AppLifecycle/AppStateMachine.swift index abe4d57f17..875090e911 100644 --- a/DuckDuckGo/AppLifecycle/AppStateMachine.swift +++ b/DuckDuckGo/AppLifecycle/AppStateMachine.swift @@ -27,6 +27,10 @@ enum AppEvent { case willResignActive case willEnterForeground +} + +enum AppAction { + case openURL(URL) case handleShortcutItem(UIApplicationShortcutItem) @@ -34,14 +38,16 @@ enum AppEvent { protocol AppState { - mutating func apply(event: AppEvent) -> any AppState + func apply(event: AppEvent) -> any AppState + mutating func handle(action: AppAction) } +@MainActor protocol AppEventHandler { - @MainActor func handle(_ event: AppEvent) + func handle(_ action: AppAction) } @@ -54,4 +60,8 @@ final class AppStateMachine: AppEventHandler { currentState = currentState.apply(event: event) } + func handle(_ action: AppAction) { + currentState.handle(action: action) + } + } diff --git a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift index e25ceb8bb0..310a5872fb 100644 --- a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift +++ b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift @@ -38,18 +38,12 @@ extension Initializing { extension Launching { - mutating func apply(event: AppEvent) -> any AppState { + func apply(event: AppEvent) -> any AppState { switch event { case .didBecomeActive: return Foreground(stateContext: makeStateContext()) case .didEnterBackground: return Background(stateContext: makeStateContext()) - case .openURL(let url): - urlToOpen = url - return self - case .handleShortcutItem(let shortcutItem): - shortcutItemToHandle = shortcutItem - return self case .didFinishLaunching, .willResignActive, .willEnterForeground: return handleUnexpectedEvent(event) } @@ -63,12 +57,6 @@ extension Foreground { switch event { case .willResignActive: return Suspending(stateContext: makeStateContext()) - case .openURL(let url): - openURL(url) - return self - case .handleShortcutItem(let shortcutItem): - handleShortcutItem(shortcutItem) - return self case .didFinishLaunching, .didBecomeActive, .didEnterBackground, .willEnterForeground: return handleUnexpectedEvent(event) } @@ -78,18 +66,12 @@ extension Foreground { extension Suspending { - mutating func apply(event: AppEvent) -> any AppState { + func apply(event: AppEvent) -> any AppState { switch event { case .didEnterBackground: return Background(stateContext: makeStateContext()) case .didBecomeActive: return Foreground(stateContext: makeStateContext()) - case .openURL(let url): - urlToOpen = url - return self - case .handleShortcutItem(let shortcutItem): - shortcutItemToHandle = shortcutItem - return self case .didFinishLaunching, .willResignActive, .willEnterForeground: return handleUnexpectedEvent(event) } @@ -99,16 +81,10 @@ extension Suspending { extension Background { - mutating func apply(event: AppEvent) -> any AppState { + func apply(event: AppEvent) -> any AppState { switch event { case .willEnterForeground: return Resuming(stateContext: makeStateContext()) - case .openURL(let url): - urlToOpen = url - return self - case .handleShortcutItem(let shortcutItem): - shortcutItemToHandle = shortcutItem - return self case .didFinishLaunching, .didBecomeActive, .willResignActive, .didEnterBackground: return handleUnexpectedEvent(event) } @@ -118,18 +94,12 @@ extension Background { extension Resuming { - mutating func apply(event: AppEvent) -> any AppState { + func apply(event: AppEvent) -> any AppState { switch event { case .didBecomeActive: return Foreground(stateContext: makeStateContext()) case .didEnterBackground: return Background(stateContext: makeStateContext()) - case .openURL(let url): - urlToOpen = url - return self - case .handleShortcutItem(let shortcutItem): - shortcutItemToHandle = shortcutItem - return self case .didFinishLaunching, .willResignActive, .willEnterForeground: return handleUnexpectedEvent(event) } @@ -152,8 +122,6 @@ extension AppEvent { case .didEnterBackground: return "backgrounding" case .willResignActive: return "suspending" case .willEnterForeground: return "resuming" - case .openURL: return "openURL" - case .handleShortcutItem: return "handleShortcutItem" } } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Background.swift b/DuckDuckGo/AppLifecycle/AppStates/Background.swift index d3959f900e..8e2267fddc 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Background.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Background.swift @@ -131,3 +131,16 @@ extension Background { } } + +extension Background { + + mutating func handle(action: AppAction) { + switch action { + case .openURL(let url): + urlToOpen = url + case .handleShortcutItem(let shortcutItem): + shortcutItemToHandle = shortcutItem + } + } + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Foreground.swift b/DuckDuckGo/AppLifecycle/AppStates/Foreground.swift index c2188cfed0..aac7b80ef3 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Foreground.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Foreground.swift @@ -509,3 +509,16 @@ extension Foreground { } } + +extension Foreground { + + mutating func handle(action: AppAction) { + switch action { + case .openURL(let url): + openURL(url) + case .handleShortcutItem(let shortcutItem): + handleShortcutItem(shortcutItem) + } + } + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Initializing.swift b/DuckDuckGo/AppLifecycle/AppStates/Initializing.swift index 66cc420bed..ac04e28cfc 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Initializing.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Initializing.swift @@ -52,3 +52,10 @@ extension Initializing { } } + + +extension Initializing { + + mutating func handle(action: AppAction) { } + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Launching.swift b/DuckDuckGo/AppLifecycle/AppStates/Launching.swift index cc2738ef04..9dcc105958 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Launching.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Launching.swift @@ -640,6 +640,19 @@ extension Launching { } +extension Launching { + + mutating func handle(action: AppAction) { + switch action { + case .openURL(let url): + urlToOpen = url + case .handleShortcutItem(let shortcutItem): + shortcutItemToHandle = shortcutItem + } + } + +} + extension UIApplication { func setWindow(_ window: UIWindow?) { diff --git a/DuckDuckGo/AppLifecycle/AppStates/Resuming.swift b/DuckDuckGo/AppLifecycle/AppStates/Resuming.swift index 57b11efc79..a84c91b652 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Resuming.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Resuming.swift @@ -105,3 +105,17 @@ extension Resuming { } +extension Resuming { + + mutating func handle(action: AppAction) { + switch action { + case .openURL(let url): + urlToOpen = url + case .handleShortcutItem(let shortcutItem): + shortcutItemToHandle = shortcutItem + } + } + +} + + diff --git a/DuckDuckGo/AppLifecycle/AppStates/Suspending.swift b/DuckDuckGo/AppLifecycle/AppStates/Suspending.swift index f79031bfd7..04f8c4ca56 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Suspending.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Suspending.swift @@ -62,3 +62,16 @@ extension Suspending { } } + +extension Suspending { + + mutating func handle(action: AppAction) { + switch action { + case .openURL(let url): + urlToOpen = url + case .handleShortcutItem(let shortcutItem): + shortcutItemToHandle = shortcutItem + } + } + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Testing.swift b/DuckDuckGo/AppLifecycle/AppStates/Testing.swift index 2363721731..e9cdd258a2 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Testing.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Testing.swift @@ -45,3 +45,9 @@ struct Testing: AppState { } } + +extension Testing { + + mutating func handle(action: AppAction) { } + +} From 11c9080f1a28a97261b60fbf49a53ca2efe2f140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Wed, 15 Jan 2025 15:57:06 +0100 Subject: [PATCH 3/8] Cleanup app transitions --- .../AppLifecycle/AppStateTransitions.swift | 33 +++++-------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift index 310a5872fb..8d5dd0b127 100644 --- a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift +++ b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift @@ -25,10 +25,7 @@ extension Initializing { func apply(event: AppEvent) -> any AppState { switch event { case .didFinishLaunching(let application, let isTesting): - if isTesting { - return Testing(application: application) - } - return Launching(stateContext: makeStateContext(application: application)) + return isTesting ? Testing(application: application) : Launching(stateContext: makeStateContext(application: application)) default: return handleUnexpectedEvent(event) } @@ -44,7 +41,7 @@ extension Launching { return Foreground(stateContext: makeStateContext()) case .didEnterBackground: return Background(stateContext: makeStateContext()) - case .didFinishLaunching, .willResignActive, .willEnterForeground: + default: return handleUnexpectedEvent(event) } } @@ -57,7 +54,7 @@ extension Foreground { switch event { case .willResignActive: return Suspending(stateContext: makeStateContext()) - case .didFinishLaunching, .didBecomeActive, .didEnterBackground, .willEnterForeground: + default: return handleUnexpectedEvent(event) } } @@ -72,7 +69,7 @@ extension Suspending { return Background(stateContext: makeStateContext()) case .didBecomeActive: return Foreground(stateContext: makeStateContext()) - case .didFinishLaunching, .willResignActive, .willEnterForeground: + default: return handleUnexpectedEvent(event) } } @@ -85,7 +82,7 @@ extension Background { switch event { case .willEnterForeground: return Resuming(stateContext: makeStateContext()) - case .didFinishLaunching, .didBecomeActive, .willResignActive, .didEnterBackground: + default: return handleUnexpectedEvent(event) } } @@ -100,7 +97,7 @@ extension Resuming { return Foreground(stateContext: makeStateContext()) case .didEnterBackground: return Background(stateContext: makeStateContext()) - case .didFinishLaunching, .willResignActive, .willEnterForeground: + default: return handleUnexpectedEvent(event) } } @@ -113,27 +110,13 @@ extension Testing { } -extension AppEvent { - - var rawValue: String { - switch self { - case .didFinishLaunching: return "launching" - case .didBecomeActive: return "activating" - case .didEnterBackground: return "backgrounding" - case .willResignActive: return "suspending" - case .willEnterForeground: return "resuming" - } - } - -} - extension AppState { func handleUnexpectedEvent(_ event: AppEvent) -> Self { - Logger.lifecycle.error("Invalid transition (\(event.rawValue)) for state (\(type(of: self)))") + Logger.lifecycle.error("🔴 Unexpected [\(String(describing: event))] event while in [\(type(of: self))] state!") DailyPixel.fireDailyAndCount(pixel: .appDidTransitionToUnexpectedState, withAdditionalParameters: [PixelParameters.appState: String(describing: type(of: self)), - PixelParameters.appEvent: event.rawValue]) + PixelParameters.appEvent: String(describing: event)]) return self } From a7a8025372160b4b7de742dd9698436198de3619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Wed, 15 Jan 2025 16:12:14 +0100 Subject: [PATCH 4/8] Cleanup app transitions part 2 --- .../AppLifecycle/AppStateTransitions.swift | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift index 8d5dd0b127..709a1638c6 100644 --- a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift +++ b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift @@ -23,12 +23,8 @@ import Core extension Initializing { func apply(event: AppEvent) -> any AppState { - switch event { - case .didFinishLaunching(let application, let isTesting): - return isTesting ? Testing(application: application) : Launching(stateContext: makeStateContext(application: application)) - default: - return handleUnexpectedEvent(event) - } + guard case .didFinishLaunching(let application, let isTesting) = event else { return handleUnexpectedEvent(event) } + return isTesting ? Testing(application: application) : Launching(stateContext: makeStateContext(application: application)) } } @@ -51,12 +47,8 @@ extension Launching { extension Foreground { func apply(event: AppEvent) -> any AppState { - switch event { - case .willResignActive: - return Suspending(stateContext: makeStateContext()) - default: - return handleUnexpectedEvent(event) - } + guard case .willResignActive = event else { return handleUnexpectedEvent(event) } + return Suspending(stateContext: makeStateContext()) } } @@ -79,12 +71,8 @@ extension Suspending { extension Background { func apply(event: AppEvent) -> any AppState { - switch event { - case .willEnterForeground: - return Resuming(stateContext: makeStateContext()) - default: - return handleUnexpectedEvent(event) - } + guard case .willEnterForeground = event else { return handleUnexpectedEvent(event) } + return Resuming(stateContext: makeStateContext()) } } From 8d8804113c2fa2fea0fdf3213555825e6b84148e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Thu, 16 Jan 2025 15:17:05 +0100 Subject: [PATCH 5/8] Add docs --- DuckDuckGo/AppDelegate.swift | 5 +++++ .../AppLifecycle/AppStates/Background.swift | 12 ++++++++++++ .../AppLifecycle/AppStates/Foreground.swift | 13 ++++++++++++- .../AppLifecycle/AppStates/Initializing.swift | 4 ++++ .../AppLifecycle/AppStates/Launching.swift | 13 +++++++++++++ .../AppLifecycle/AppStates/Resuming.swift | 9 +++++++++ .../AppLifecycle/AppStates/Suspending.swift | 17 +++++++++++++++++ 7 files changed, 72 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 949a839715..7c8fd0e79e 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -37,24 +37,29 @@ import Core (appStateMachine.currentState as? Foreground)?.appDependencies.privacyProDataReporter // just for now, we have to get rid of this anti pattern } + /// See: Launching.swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let isTesting: Bool = ProcessInfo().arguments.contains("testing") appStateMachine.handle(.didFinishLaunching(application, isTesting: isTesting)) return true } + /// See: Foreground.swift func applicationDidBecomeActive(_ application: UIApplication) { appStateMachine.handle(.didBecomeActive) } + /// See: Suspending.swift func applicationWillResignActive(_ application: UIApplication) { appStateMachine.handle(.willResignActive) } + /// See: Resuming.swift func applicationWillEnterForeground(_ application: UIApplication) { appStateMachine.handle(.willEnterForeground) } + /// See: Background.swift func applicationDidEnterBackground(_ application: UIApplication) { appStateMachine.handle(.didEnterBackground) } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Background.swift b/DuckDuckGo/AppLifecycle/AppStates/Background.swift index 8e2267fddc..7b3a6496b6 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Background.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Background.swift @@ -23,6 +23,18 @@ import DDGSync import UIKit import Core +/// Represents the state where the app is fully in the background and not visible to the user. +/// - Usage: +/// - This state is typically associated with the `applicationDidEnterBackground(_:)` method. +/// - The app transitions to this state when it is no longer in the foreground, either due to the user +/// minimizing the app, switching to another app, or locking the device. +/// - Transitions: +/// - `Resuming`: The app transitions to the `Resuming` state when the user brings the app back to the foreground. +/// - Notes: +/// - This is one of the app's two long-lived states, alongside `Foreground`. +/// - Background tasks, such as saving data or refreshing content, should be handled in this state. +/// - Use this state to ensure that the app's current state is saved and any necessary cleanup is performed +/// to release resources or prepare for a potential termination. struct Background: AppState { private let lastBackgroundDate: Date = Date() diff --git a/DuckDuckGo/AppLifecycle/AppStates/Foreground.swift b/DuckDuckGo/AppLifecycle/AppStates/Foreground.swift index aac7b80ef3..30807257c0 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Foreground.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Foreground.swift @@ -26,6 +26,17 @@ import BackgroundTasks import Subscription import NetworkProtection +/// Represents the state where the app is active and available for user interaction. +/// - Usage: +/// - This state is typically associated with the `applicationDidBecomeActive(_:)` method. +/// - The app transitions to this state after completing the launch process or resuming from the background. +/// - During this state, the app is fully interactive, and the user can engage with the app's UI. +/// - Transitions: +/// - `Suspending`: The app transitions to this state when it begins the process of moving to the background, +/// typically triggered by the `applicationWillResignActive(_:)` method, e.g.: +/// - When the user presses the home button, swipes up to the App Switcher, or receives a system interruption. +/// - Notes: +/// - This is one of the two long-living states in the app's lifecycle (along with `Background`). struct Foreground: AppState { let application: UIApplication @@ -512,7 +523,7 @@ extension Foreground { extension Foreground { - mutating func handle(action: AppAction) { + func handle(action: AppAction) { switch action { case .openURL(let url): openURL(url) diff --git a/DuckDuckGo/AppLifecycle/AppStates/Initializing.swift b/DuckDuckGo/AppLifecycle/AppStates/Initializing.swift index ac04e28cfc..5f07b9ba8b 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Initializing.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Initializing.swift @@ -21,6 +21,10 @@ import Core import Crashes import UIKit +/// The initial setup phase of the app, where basic services or components are initialized. +/// This state can be invoked when the system prewarms the app but does not fully launch it. +/// - Transitions: +/// - `Launching` after initialization is complete. @MainActor struct Initializing: AppState { diff --git a/DuckDuckGo/AppLifecycle/AppStates/Launching.swift b/DuckDuckGo/AppLifecycle/AppStates/Launching.swift index 9dcc105958..2eb3a74e38 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Launching.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Launching.swift @@ -35,6 +35,19 @@ import Combine import PixelKit import PixelExperimentKit +/// Represents the transient state where the app is being prepared for user interaction after being launched by the system. +/// - Usage: +/// - This state is typically associated with the `application(_:didFinishLaunchingWithOptions:)` method. +/// - It is responsible for performing the app's initial setup, including configuring dependencies and preparing the UI. +/// - As part of this state, the `MainViewController` is created and set as the `rootViewController` of the app's primary `UIWindow`. +/// - Transitions: +/// - `Foreground`: Standard transition when the app completes its launch process and becomes active. +/// - `Background`: Occurs when the app is launched but transitions directly to the background, e.g: +/// - The app is protected by a FaceID lock mechanism (introduced in iOS 18.0). If the user opens the app +/// but does not authenticate and then leaves. +/// - The app is launched by the system for background execution but does not immediately become active. +/// - Notes: +/// - Avoid performing heavy or blocking operations during this phase to ensure smooth app startup. @MainActor struct Launching: AppState { diff --git a/DuckDuckGo/AppLifecycle/AppStates/Resuming.swift b/DuckDuckGo/AppLifecycle/AppStates/Resuming.swift index a84c91b652..bd0eacd3ab 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Resuming.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Resuming.swift @@ -19,6 +19,15 @@ import UIKit +/// Represents the transient state where the app is resuming after being backgrounded, preparing to return to the foreground. +/// - Usage: +/// - This state is typically associated with the `applicationWillEnterForeground(_:)` method. +/// - The app transitions to this state when the system sends the `willEnterForeground` event, +/// indicating that the app is about to become active again. +/// - Transitions: +/// - `Foreground`: The app transitions to the `Foreground` state when the app is fully active and visible to the user after resuming. +/// - `Background`: The app can transition to the `Background` state if, +/// e.g. the app is protected by a FaceID lock mechanism (introduced in iOS 18.0) and the user does not authenticate and then leaves. struct Resuming: AppState { private let application: UIApplication diff --git a/DuckDuckGo/AppLifecycle/AppStates/Suspending.swift b/DuckDuckGo/AppLifecycle/AppStates/Suspending.swift index 04f8c4ca56..11944b7ea8 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Suspending.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Suspending.swift @@ -19,6 +19,23 @@ import UIKit +/// Represents the transient state where the app is in the process of moving out of the foreground. +/// - Usage: +/// - This state is typically associated with the `applicationWillResignActive(_:)` method. +/// - The app transitions to this state when it is temporarily interrupted or begins moving to the background. +/// - Common triggers include: +/// - The user receives a system notification. +/// - The user accesses the App Switcher. +/// - The user starts swiping up to exit the app but does not complete the gesture. +/// - Transitions: +/// - `Foreground`: The app transitions back to `Foreground` if the user dismisses the system notification or +/// returns from the App Switcher to the app without fully transitioning to the background. +/// - `Background`: The app transitions to `Background` if the user completes the gesture to move the app out +/// of the foreground or another action causes the app to enter the background. +/// - Notes: +/// - This is a short-lived state and is part of the app's standard lifecycle. +/// - It allows the app to prepare for a potential transition to `Background`, such as pausing animations +/// or saving transient state information. struct Suspending: AppState { private let application: UIApplication From f2860f9ad674c8449667a0f4a0c52a7bc355db96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Thu, 16 Jan 2025 15:36:20 +0100 Subject: [PATCH 6/8] Add more documentation --- DuckDuckGo/AppLifecycle/AppStates/Background.swift | 11 +++++++++-- DuckDuckGo/AppLifecycle/AppStates/Foreground.swift | 8 +++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/AppLifecycle/AppStates/Background.swift b/DuckDuckGo/AppLifecycle/AppStates/Background.swift index 7b3a6496b6..a28b72a4f1 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Background.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Background.swift @@ -44,7 +44,10 @@ struct Background: AppState { var urlToOpen: URL? var shortcutItemToHandle: UIApplicationShortcutItem? - init(stateContext: Suspending.StateContext) { + // MARK: Handle logic when transitioning from Launching to Background + // This transition can occur if the app is protected by FaceID (e.g., the app is launched, but the user doesn't authenticate). + // Note: In this case, the Foreground state was never shown to the user, so you may want to avoid ending sessions that were never started, etc. + init(stateContext: Launching.StateContext) { application = stateContext.application appDependencies = stateContext.appDependencies urlToOpen = stateContext.urlToOpen @@ -52,7 +55,9 @@ struct Background: AppState { run() } - init(stateContext: Launching.StateContext) { + // MARK: Handle logic when transitioning from Suspending to Background + // This transition occurs when the app moves from foreground to the background. + init(stateContext: Suspending.StateContext) { application = stateContext.application appDependencies = stateContext.appDependencies urlToOpen = stateContext.urlToOpen @@ -60,6 +65,8 @@ struct Background: AppState { run() } + // MARK: Handle logic when transitioning from Resuming to Background + // This transition can occur when the app returns to the background after being in the background (e.g., user doesn't authenticate on a locked app). init(stateContext: Resuming.StateContext) { application = stateContext.application appDependencies = stateContext.appDependencies diff --git a/DuckDuckGo/AppLifecycle/AppStates/Foreground.swift b/DuckDuckGo/AppLifecycle/AppStates/Foreground.swift index 30807257c0..401748910e 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Foreground.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Foreground.swift @@ -53,7 +53,9 @@ struct Foreground: AppState { appDependencies.mainViewController } - // MARK: handle one-time (after launch) logic here + // MARK: Handle logic when transitioning from Launched to Foreground + // This transition occurs when the app has completed its launch process and becomes active. + // Note: You want to add here code that will happen one-time per app lifecycle, but you need the UI to be active at this point! init(stateContext: Launching.StateContext) { application = stateContext.application appDependencies = stateContext.appDependencies @@ -96,6 +98,8 @@ struct Foreground: AppState { activateApp() } + // MARK: Handle logic when transitioning from Resuming to Foreground + // This transition occurs when the app returns to the foreground after being backgrounded (e.g., after unlocking the app). init(stateContext: Resuming.StateContext) { application = stateContext.application appDependencies = stateContext.appDependencies @@ -110,6 +114,8 @@ struct Foreground: AppState { activateApp() } + // MARK: Handle logic when transitioning from Suspending to Foreground + // This transition occurs when the app returns to the foreground after briefly being suspended (e.g., user dismisses a notification). init(stateContext: Suspending.StateContext) { application = stateContext.application appDependencies = stateContext.appDependencies From 2b2c4881befccbf35fd22f550baa2f07bd6f3610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Thu, 16 Jan 2025 17:01:53 +0100 Subject: [PATCH 7/8] Fix swiftlint --- DuckDuckGo/AppLifecycle/AppStates/Resuming.swift | 2 -- DuckDuckGoTests/AppSettingsMock.swift | 1 - 2 files changed, 3 deletions(-) diff --git a/DuckDuckGo/AppLifecycle/AppStates/Resuming.swift b/DuckDuckGo/AppLifecycle/AppStates/Resuming.swift index bd0eacd3ab..2ea6df5064 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Resuming.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Resuming.swift @@ -126,5 +126,3 @@ extension Resuming { } } - - diff --git a/DuckDuckGoTests/AppSettingsMock.swift b/DuckDuckGoTests/AppSettingsMock.swift index b81eba6371..c38dc19e94 100644 --- a/DuckDuckGoTests/AppSettingsMock.swift +++ b/DuckDuckGoTests/AppSettingsMock.swift @@ -22,7 +22,6 @@ import Foundation @testable import DuckDuckGo class AppSettingsMock: AppSettings { - var appBehavior: DuckDuckGo.AppBehavior? = .new var defaultTextZoomLevel: DuckDuckGo.TextZoomLevel = .percent100 From 9483c31327b8aa2a07581b1efddc3688dd4a604f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Thu, 16 Jan 2025 17:21:33 +0100 Subject: [PATCH 8/8] Make sure refreshRemoteMessages works --- DuckDuckGo/AppDelegate.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 7c8fd0e79e..22f501ff0a 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -33,10 +33,6 @@ import Core var window: UIWindow? - var privacyProDataReporter: PrivacyProDataReporting? { - (appStateMachine.currentState as? Foreground)?.appDependencies.privacyProDataReporter // just for now, we have to get rid of this anti pattern - } - /// See: Launching.swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let isTesting: Bool = ProcessInfo().arguments.contains("testing") @@ -93,9 +89,18 @@ import Core true } + var privacyProDataReporter: PrivacyProDataReporting? { + (appStateMachine.currentState as? Foreground)?.appDependencies.privacyProDataReporter // just for now, we have to get rid of this anti pattern + } + /// It's public in order to allow refreshing on demand via Debug menu. Otherwise it shouldn't be called from outside. + /// we have to get rid of this anti pattern func refreshRemoteMessages() { - // part of debug menu, let's not support it in the first iteration + Task { + if let remoteMessagingClient = (appStateMachine.currentState as? Foreground)?.appDependencies.remoteMessagingClient { + try? await remoteMessagingClient.fetchAndProcess(using: remoteMessagingClient.store) + } + } } }