Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions WordPress/Classes/System/UITesting/UITestConfigurator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ struct UITestConfigurator {
CommandLine.arguments.contains("-ui-testing")
}

static func prepareApplicationForUITests(in app: UIApplication, window: UIWindow) {
/// Applies the process-scoped part of the UI-test setup: flag parsing and, when
/// requested, wiping persistent state. Must run at the very start of the launch
/// sequence, before anything reads the flags or touches Core Data and user defaults.
static func prepareApplicationForUITests() {
let arguments = CommandLine.arguments
if arguments.contains("-ui-testing") {
flags.insert(.disableLogging)
Expand All @@ -23,7 +26,7 @@ struct UITestConfigurator {
}
if arguments.contains("-ui-test-disable-animations") {
flags.insert(.disableAnimations)
disableAnimations(in: app, window: window)
UIView.setAnimationsEnabled(false)
}
if arguments.contains("-ui-test-use-mock-data") {
flags.insert(.useMockData)
Expand All @@ -37,11 +40,13 @@ struct UITestConfigurator {
}
}

/// This method will disable animations and speed-up keyboad input if command-line arguments includes "NoAnimations"
/// It was designed to be used in UI test suites. To enable it just pass a launch argument into XCUIApplicaton.
private static func disableAnimations(in app: UIApplication, window: UIWindow) {
UIView.setAnimationsEnabled(false)
window.layer.speed = MAXFLOAT
/// Applies the window-scoped part of the UI-test setup. Runs whenever the main
/// window is created, which (under the scene life cycle) happens at scene connect,
/// after `prepareApplicationForUITests()` already parsed the flags.
static func prepareWindowForUITests(_ window: UIWindow) {
if isEnabled(.disableAnimations) {
window.layer.speed = MAXFLOAT
}
}

private static func resetEverything() {
Expand Down
10 changes: 1 addition & 9 deletions WordPress/Classes/System/WordPressAppDelegate+openURL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,8 @@ import WordPressData
import WordPressShared

@objc extension WordPressAppDelegate {
public func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
handle(url: url)
}

/// Routes an inbound URL (custom scheme deep links, magic-login, migration, OAuth).
/// Reused by both the legacy app-lifecycle path and `WordPressSceneDelegate`.
/// Called by `WordPressSceneDelegate` for inbound URLs.
@discardableResult
func handle(url: URL) -> Bool {
let redactedURL = LoggingURLRedactor.redactedURL(url)
Expand Down
180 changes: 117 additions & 63 deletions WordPress/Classes/System/WordPressAppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,15 @@ public class WordPressAppDelegate: UIResponder, UIApplicationDelegate {

@objc
lazy var windowManager: WindowManager = {
guard let window else {
fatalError("The App cannot run without a window.")
}
if window == nil {
// Reachable only when a code path builds UI before any scene has connected
// (e.g. during a background launch); such paths should be guarded instead.
// Recover by creating the window now: `WordPressSceneDelegate` attaches it to
// the scene once one connects.
assertionFailure("windowManager accessed before any scene connected")
window = UIWindow(frame: UIScreen.main.bounds)
}
let window = window!
return switch BuildSettings.current.brand {
case .wordpress: WindowManager(window: window)
case .jetpack: JetpackWindowManager(window: window)
Expand Down Expand Up @@ -94,18 +100,14 @@ public class WordPressAppDelegate: UIResponder, UIApplicationDelegate {
])
}

let window = UIWindow(frame: UIScreen.main.bounds)
self.window = window

WordPressAPIInternal.setupLogger(appId: Bundle.main.bundleIdentifier!)
DesignSystem.FontManager.registerCustomFonts()
AssertionLoggerDependencyContainer.logger = AssertionLogger()
UITestConfigurator.prepareApplicationForUITests(in: application, window: window)
UITestConfigurator.prepareApplicationForUITests()

// The following extensive logging configuration detects if extensive logging is enabled internally.
wpkURLSessionNotifyingDelegate = PulseNetworkLogger()

AppAppearance.overrideAppearance()
MemoryCache.shared.register()
MediaImageService.migrateCacheIfNeeded()
PostCoordinator.shared.delegate = self
Expand All @@ -116,27 +118,98 @@ public class WordPressAppDelegate: UIResponder, UIApplicationDelegate {
// Configure WPCom API overrides
configureWordPressComApi()

configureWordPressAuthenticator()

ReachabilityUtils.configure()

configureSelfHostedChallengeHandler()
updateFeatureFlags()
updateRemoteConfig()

window.makeKeyAndVisible()

// Restore a disassociated account prior to fixing tokens.
AccountService(coreDataStack: ContextManager.shared).restoreDisassociatedAccountIfNecessary()

customizeAppearance()
configureAnalytics()

runStartupSequence(with: launchOptions ?? [:])
runStartupSequence()

return true
}

/// Opts the app into the UIScene life cycle and names the scene delegate. Doing
/// this in code (instead of a UIApplicationSceneManifest in each Info.plist)
/// keeps the configuration in one place for all three app targets. The unit test
/// host runs TestingAppDelegate, which doesn't implement this method, so tests
/// keep the legacy life cycle.
public func application(
_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions
) -> UISceneConfiguration {
let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
configuration.delegateClass = WordPressSceneDelegate.self
return configuration
}

/// Whether `showInitialUI(in:)` has already built the UI. The scene can disconnect
/// and reconnect while the process stays alive, so the build must run exactly once.
private(set) var hasConfiguredInitialUI = false

/// Set during the first scene connect, because `sceneWillEnterForeground` also
/// fires right after it. The legacy `applicationWillEnterForeground` did not fire
/// during a cold launch, and `handleWillEnterForeground` keeps those semantics.
private var isInitialSceneConnect = false

/// Builds and shows the app's UI in the given window scene. Runs when the main
/// scene connects, which (under the scene life cycle) is after `didFinishLaunching`
/// and can happen multiple times per process: the system may disconnect the scene
/// in the background and reconnect it later. After the first connect, the existing
/// window (with all its UI state) is simply reattached to the new scene.
func showInitialUI(in windowScene: UIWindowScene) {
if hasConfiguredInitialUI {
window?.windowScene = windowScene
window?.makeKeyAndVisible()
return
}
hasConfiguredInitialUI = true
isInitialSceneConnect = true

let window: UIWindow
if let existingWindow = self.window {
// Created by the `windowManager` emergency fallback before any scene
// connected; adopt it instead of orphaning the UI already built in it.
existingWindow.windowScene = windowScene
window = existingWindow
} else {
window = UIWindow(windowScene: windowScene)
self.window = window
}

UITestConfigurator.prepareWindowForUITests(window)

// Must run here (not in willFinishLaunching): it accesses `windowManager`,
// which requires `window`. startObservingAppleIDCredentialRevoked also
// requires the authenticator to be initialized first. A background-only
// launch therefore runs without the revocation observer; the
// `checkAppleIDCredentialState` call in `handleDidBecomeActive` covers the
// gap at the next foreground.
configureWordPressAuthenticator()
startObservingAppleIDCredentialRevoked()
customizeAppearance()
// Reapplies the user's saved light/dark override; reads the app's window, so it
// can only take effect once the window exists.
AppAppearance.overrideAppearance()

window.makeKeyAndVisible()

// The 3D Touch / Home Screen quick actions probe the trait collection of
// `UIApplication.shared.mainWindow`, which only exists once the window above is key.
shortcutCreator.createShortcutsIf3DTouchAvailable(AccountHelper.isLoggedIn)

windowManager.showUI()
restoreAppState()

DebugMenuViewController.configure(in: window)
}

public func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
Expand All @@ -154,7 +227,6 @@ public class WordPressAppDelegate: UIResponder, UIApplicationDelegate {
// (e.g. the share extension's background URL session) post notices that it
// delivers as local notifications, with no UI visible.
setupNoticePresenter()
DebugMenuViewController.configure(in: window)
AppTips.initialize()

// This was necessary to properly load fonts for the Stories editor. I believe external libraries may require this call to access fonts.
Expand All @@ -164,8 +236,6 @@ public class WordPressAppDelegate: UIResponder, UIApplicationDelegate {
CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil)
})

startObservingAppleIDCredentialRevoked()

NotificationCenter.default.post(name: .applicationLaunchCompleted, object: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
WKWebView.warmup()
Expand All @@ -183,22 +253,6 @@ public class WordPressAppDelegate: UIResponder, UIApplicationDelegate {
Loggers.app.info("\(self) \(#function)")
}

public func applicationDidEnterBackground(_ application: UIApplication) {
handleDidEnterBackground()
}

public func applicationWillEnterForeground(_ application: UIApplication) {
handleWillEnterForeground()
}

public func applicationWillResignActive(_ application: UIApplication) {
handleWillResignActive()
}

public func applicationDidBecomeActive(_ application: UIApplication) {
handleDidBecomeActive()
}

// MARK: - Activation handlers (shared by app- and scene-lifecycle)

func handleDidEnterBackground() {
Expand Down Expand Up @@ -232,9 +286,21 @@ public class WordPressAppDelegate: UIResponder, UIApplicationDelegate {
func handleWillEnterForeground() {
Loggers.app.info("\(self) \(#function)")

// Refresh on every foreground, including the one right after the initial
// scene connect: a background-launched process may sit for hours between
// the launch-time refresh and the user opening the app.
updateFeatureFlags()
updateRemoteConfig()

// Skip the migration check on the call that follows the initial scene
// connect: the window phase already ran it via `windowManager.showUI()`,
// and running it twice emits its analytics event twice per launch. Scene
// reconnects are a real return from the background and fall through.
if isInitialSceneConnect {
isInitialSceneConnect = false
return
}

// JetpackWindowManager is only available in the Jetpack target.
if let windowManager = windowManager as? JetpackWindowManager {
windowManager.startMigrationFlowIfNeeded()
Expand All @@ -256,15 +322,6 @@ public class WordPressAppDelegate: UIResponder, UIApplicationDelegate {
GutenbergSettings().performGutenbergPhase2MigrationIfNeeded()
}

public func application(
_ application: UIApplication,
performActionFor shortcutItem: UIApplicationShortcutItem,
completionHandler: @escaping (Bool) -> Void
) {
let handler = WP3DTouchShortcutHandler()
completionHandler(handler.handleShortcutItem(shortcutItem))
}

public func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
Expand All @@ -280,15 +337,6 @@ public class WordPressAppDelegate: UIResponder, UIApplicationDelegate {
}
}

public func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
handle(userActivity: userActivity)
return true
}

/// Routes an inbound `NSUserActivity` (universal links via web browsing, Spotlight, Handoff).
func handle(userActivity: NSUserActivity) {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
Expand Down Expand Up @@ -316,7 +364,7 @@ public class WordPressAppDelegate: UIResponder, UIApplicationDelegate {

// MARK: - Setup

func runStartupSequence(with launchOptions: [UIApplication.LaunchOptionsKey: Any] = [:]) {
func runStartupSequence() {
// Local notifications
addNotificationObservers()

Expand All @@ -329,7 +377,7 @@ public class WordPressAppDelegate: UIResponder, UIApplicationDelegate {
TracksLogging.delegate = libraryLogger
WPKitSetLoggingDelegate(libraryLogger)
WPSetLoggingDelegate(libraryLogger)
printDebugLaunchInfoWithLaunchOptions(launchOptions)
printDebugLaunchInfo()
toggleExtraDebuggingIfNeeded()
}

Expand Down Expand Up @@ -366,12 +414,7 @@ public class WordPressAppDelegate: UIResponder, UIApplicationDelegate {

setupWordPressExtensions()

shortcutCreator.createShortcutsIf3DTouchAvailable(AccountHelper.isLoggedIn)

AccountService.loadDefaultAccountCookies()

windowManager.showUI()
restoreAppState()
}

private func mergeDuplicateAccountsIfNeeded() {
Expand Down Expand Up @@ -630,7 +673,7 @@ extension WordPressAppDelegate {
// MARK: - Debugging

extension WordPressAppDelegate {
func printDebugLaunchInfoWithLaunchOptions(_ launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) {
func printDebugLaunchInfo() {
let unknown = "Unknown"

let device = UIDevice.current
Expand Down Expand Up @@ -665,7 +708,6 @@ extension WordPressAppDelegate {
Loggers.app.info("Language: \(currentLanguage)")
Loggers.app.info("UDID: \(udid)")
Loggers.app.info("APN token: \(PushNotificationsManager.shared.deviceToken ?? "None")")
Loggers.app.info("Launch options: \(String(describing: launchOptions ?? [:]))")

AccountHelper.logBlogsAndAccounts(context: mainContext)
Loggers.app.info("===========================================================================")
Expand Down Expand Up @@ -745,14 +787,26 @@ extension WordPressAppDelegate {
// If the notification object is not nil, then it's a login
if notification.object != nil {
setupWordPressExtensions()
startObservingAppleIDCredentialRevoked()
// Pre-UI logins (the observer requires the authenticator, which is
// configured at first scene connect) are covered by `showInitialUI`,
// which registers the observer itself.
if hasConfiguredInitialUI {
startObservingAppleIDCredentialRevoked()
}
AccountService.loadDefaultAccountCookies()
} else {
trackLogoutIfNeeded()
removeShareExtensionConfiguration()
removeNotificationExtensionConfiguration()
windowManager.showFullscreenSignIn()
stopObservingAppleIDCredentialRevoked()
// Skip when the UI was never built (e.g. the account is removed during a
// background launch): there is no UI to show sign-in in, `showInitialUI`
// shows it anyway once a scene connects and finds no account, and the
// Apple ID observer was never registered (stopping it would hit
// `WordPressAuthenticator.shared` before the authenticator exists).
if hasConfiguredInitialUI {
windowManager.showFullscreenSignIn()
stopObservingAppleIDCredentialRevoked()
}
clearReaderStuff()
}

Expand Down
Loading