Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add workaround for failed payments on iOS 18.2 #4610

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
33 changes: 30 additions & 3 deletions Sources/FoundationExtensions/UIApplication+RCExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
13 changes: 12 additions & 1 deletion Sources/Misc/SystemInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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, *)
Expand All @@ -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

Expand Down
16 changes: 16 additions & 0 deletions Sources/Purchasing/Purchases/PurchasesOrchestrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down