From ec175833642df04d4189f1e74ecb3901ec5185f8 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 18 Dec 2024 21:33:57 +0100 Subject: [PATCH 1/4] Add workaround for failed payments on iOS 18.2 --- .../UIApplication+RCExtensions.swift | 21 +++++++++++++++++++ Sources/Misc/SystemInfo.swift | 11 ++++++++++ .../Purchases/PurchasesOrchestrator.swift | 16 ++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/Sources/FoundationExtensions/UIApplication+RCExtensions.swift b/Sources/FoundationExtensions/UIApplication+RCExtensions.swift index 9b15b47bcc..8b1d430209 100644 --- a/Sources/FoundationExtensions/UIApplication+RCExtensions.swift +++ b/Sources/FoundationExtensions/UIApplication+RCExtensions.swift @@ -38,6 +38,27 @@ extension UIApplication { return scenes.first as? UIWindowScene } + @available(iOS 15.0, *) + var currentViewController: UIViewController? { + guard let rootViewController = currentWindowScene?.keyWindow?.rootViewController else { + return nil + } + return getTopViewController(from: rootViewController) + } + + 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 { + if 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..306c81fc6d 100644 --- a/Sources/Misc/SystemInfo.swift +++ b/Sources/Misc/SystemInfo.swift @@ -244,6 +244,17 @@ extension SystemInfo { } } + @available(iOS 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 From 0ddedb4853951e5f95b935a90e49839f7135de96 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Wed, 18 Dec 2024 21:57:12 +0100 Subject: [PATCH 2/4] add availability for tvOS --- .../FoundationExtensions/UIApplication+RCExtensions.swift | 7 +++---- Sources/Misc/SystemInfo.swift | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Sources/FoundationExtensions/UIApplication+RCExtensions.swift b/Sources/FoundationExtensions/UIApplication+RCExtensions.swift index 8b1d430209..96f1cc3d03 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,7 +37,7 @@ extension UIApplication { return scenes.first as? UIWindowScene } - @available(iOS 15.0, *) + @available(iOS 15.0, tvOS 15.0, *) var currentViewController: UIViewController? { guard let rootViewController = currentWindowScene?.keyWindow?.rootViewController else { return nil diff --git a/Sources/Misc/SystemInfo.swift b/Sources/Misc/SystemInfo.swift index 306c81fc6d..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,7 +244,7 @@ extension SystemInfo { } } - @available(iOS 15.0, *) + @available(iOS 15.0, tvOS 15.0, *) @MainActor var currentViewController: UIViewController { get throws { From 9d4c4bd3a4fb86a1e7f1e0521f8b3b81306b4d7b Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Thu, 19 Dec 2024 13:25:44 +0100 Subject: [PATCH 3/4] lint --- .../FoundationExtensions/UIApplication+RCExtensions.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/FoundationExtensions/UIApplication+RCExtensions.swift b/Sources/FoundationExtensions/UIApplication+RCExtensions.swift index 96f1cc3d03..fe6cec02e9 100644 --- a/Sources/FoundationExtensions/UIApplication+RCExtensions.swift +++ b/Sources/FoundationExtensions/UIApplication+RCExtensions.swift @@ -50,10 +50,9 @@ extension UIApplication { return getTopViewController(from: presentedViewController) } else if let navigationController = viewController as? UINavigationController { return navigationController.visibleViewController - } else if let tabBarController = viewController as? UITabBarController { - if let selected = tabBarController.selectedViewController { - return getTopViewController(from: selected) - } + } else if let tabBarController = viewController as? UITabBarController, + let selected = tabBarController.selectedViewController { + return getTopViewController(from: selected) } return viewController } From 26c5a6a44c976431e7aedad98543577ffea13ce3 Mon Sep 17 00:00:00 2001 From: Mark Villacampa Date: Sat, 21 Dec 2024 12:20:58 +0100 Subject: [PATCH 4/4] Add fallback for application extensions like Messages extensions where scenes are not supported --- .../UIApplication+RCExtensions.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Sources/FoundationExtensions/UIApplication+RCExtensions.swift b/Sources/FoundationExtensions/UIApplication+RCExtensions.swift index fe6cec02e9..51ae2cd3e6 100644 --- a/Sources/FoundationExtensions/UIApplication+RCExtensions.swift +++ b/Sources/FoundationExtensions/UIApplication+RCExtensions.swift @@ -39,10 +39,18 @@ extension UIApplication { @available(iOS 15.0, tvOS 15.0, *) var currentViewController: UIViewController? { - guard let rootViewController = currentWindowScene?.keyWindow?.rootViewController else { + 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: rootViewController) + + return getTopViewController(from: resolvedRootViewController) } private func getTopViewController(from viewController: UIViewController) -> UIViewController? {