diff --git a/Sources/FoundationExtensions/UIApplication+RCExtensions.swift b/Sources/FoundationExtensions/UIApplication+RCExtensions.swift index 9b15b47bcc..51ae2cd3e6 100644 --- a/Sources/FoundationExtensions/UIApplication+RCExtensions.swift +++ b/Sources/FoundationExtensions/UIApplication+RCExtensions.swift @@ -11,16 +11,15 @@ // // Created by Andrés Boedo on 8/20/21. -#if os(iOS) || VISION_OS +#if os(iOS) || os(tvOS) || VISION_OS import UIKit extension UIApplication { - @available(iOS 13.0, macCatalyst 13.1, *) + @available(iOS 13.0, macCatalyst 13.1, tvOS 13.0, *) @available(macOS, unavailable) @available(watchOS, unavailable) @available(watchOSApplicationExtension, unavailable) - @available(tvOS, unavailable) @MainActor var currentWindowScene: UIWindowScene? { var scenes = self @@ -38,6 +37,34 @@ extension UIApplication { return scenes.first as? UIWindowScene } + @available(iOS 15.0, tvOS 15.0, *) + var currentViewController: UIViewController? { + var rootViewController = currentWindowScene?.keyWindow?.rootViewController + + if rootViewController == nil { + // Fallback for application extensions where scenes are not supported + rootViewController = (value(forKey: "keyWindow") as? UIWindow)?.rootViewController + } + + guard let resolvedRootViewController = rootViewController else { + return nil + } + + return getTopViewController(from: resolvedRootViewController) + } + + private func getTopViewController(from viewController: UIViewController) -> UIViewController? { + if let presentedViewController = viewController.presentedViewController { + return getTopViewController(from: presentedViewController) + } else if let navigationController = viewController as? UINavigationController { + return navigationController.visibleViewController + } else if let tabBarController = viewController as? UITabBarController, + let selected = tabBarController.selectedViewController { + return getTopViewController(from: selected) + } + return viewController + } + } #endif diff --git a/Sources/Misc/SystemInfo.swift b/Sources/Misc/SystemInfo.swift index 4c829d386d..26c968571f 100644 --- a/Sources/Misc/SystemInfo.swift +++ b/Sources/Misc/SystemInfo.swift @@ -227,7 +227,7 @@ class SystemInfo { } -#if os(iOS) || VISION_OS +#if os(iOS) || os(tvOS) || VISION_OS extension SystemInfo { @available(iOS 13.0, macCatalystApplicationExtension 13.1, *) @@ -244,6 +244,17 @@ extension SystemInfo { } } + @available(iOS 15.0, tvOS 15.0, *) + @MainActor + var currentViewController: UIViewController { + get throws { + let viewController = self.sharedUIApplication?.currentViewController + + return try viewController + .orThrow(ErrorUtils.storeProblemError(withMessage: "Failed to get UIViewController")) + } + } + } #endif diff --git a/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift b/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift index fd99199b8a..7c5697d73e 100644 --- a/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift +++ b/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift @@ -654,6 +654,22 @@ final class PurchasesOrchestrator { #if VISION_OS return try await product.purchase(confirmIn: try self.systemInfo.currentWindowScene, options: options) + #elseif (os(iOS) || os(tvOS)) && compiler(>=6.0.3) + // iOS 18.2 introduces a new `purchase(confirmIn:options:)` method which accepts a UIViewController + // This new method is present starting on Xcode 16.2 which bundles Swift 6.0.3. + // The old `purchase(options:)` method uses some heuristics to retrieve the rootViewController of the + // key window of the currerntly active scene. + // However, it is possible the rootViewController's view is currently removed from the view hieararchy. + // For example, if there is a view controller with `modalPresentationStyle = .fullScreen` being presented. + // This will prevent the payment sheet from appearing. + // To workaround this we traverse the view controller hierarchy to try to find the current top-most one. + if #available(iOS 18.2, tvOS 18.2, *), + let currentViewController = try? await self.systemInfo.currentViewController { + return try await product.purchase(confirmIn: currentViewController, + options: options) + } else { + return try await product.purchase(options: options) + } #else return try await product.purchase(options: options) #endif