diff --git a/Sources/FormbricksSDK/Helpers/FormbricksEnvironment.swift b/Sources/FormbricksSDK/Helpers/FormbricksEnvironment.swift index 3111117..dc2974a 100644 --- a/Sources/FormbricksSDK/Helpers/FormbricksEnvironment.swift +++ b/Sources/FormbricksSDK/Helpers/FormbricksEnvironment.swift @@ -20,7 +20,8 @@ internal enum FormbricksEnvironment { /// Returns the full environment‐fetch URL as a String for the given ID static var getEnvironmentRequestEndpoint: String { - return ["api", "v2", "client", "{environmentId}", "environment"].joined(separator: "/") + let path = ["api", "v2", "client", "{environmentId}", "environment"].joined(separator: "/") + return path } /// Returns the full post-user URL as a String for the given ID diff --git a/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift b/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift index 5875cf8..fc22e72 100644 --- a/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift +++ b/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift @@ -1,6 +1,6 @@ import SwiftUI -/// Presents a survey webview to the window's root +/// Presents a survey webview from the top-most view controller in the key window. final class PresentSurveyManager { init() { /* @@ -8,39 +8,70 @@ final class PresentSurveyManager { The class serves as a namespace for the present method, so instance creation is not needed and should be restricted. */ } - + /// The view controller that will present the survey window. private weak var viewController: UIViewController? - - /// Present the webview - /// The native background is always `.clear` — overlay rendering is handled - /// entirely by the JS survey library inside the WebView to avoid double-overlay artifacts. + + /// Walks the active presentation/navigation/tab hierarchy and returns the leaf VC. + /// Mirrors UIKit's own `presentedViewController` traversal so a single walker is enough. + private func topMostViewController(from viewController: UIViewController) -> UIViewController { + if let presented = viewController.presentedViewController, + !presented.isBeingDismissed { + return topMostViewController(from: presented) + } + if let navigation = viewController as? UINavigationController, + let visible = navigation.visibleViewController { + return topMostViewController(from: visible) + } + if let tabBar = viewController as? UITabBarController, + let selected = tabBar.selectedViewController { + return topMostViewController(from: selected) + } + return viewController + } + + /// Present the webview as a page sheet over the current top-most view controller. func present(environmentResponse: EnvironmentResponse, id: String, completion: ((Bool) -> Void)? = nil) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } - if let window = UIApplication.safeKeyWindow { - let view = FormbricksView(viewModel: FormbricksViewModel(environmentResponse: environmentResponse, surveyId: id)) - let vc = UIHostingController(rootView: view) - vc.modalPresentationStyle = .overCurrentContext - vc.view.backgroundColor = .clear - if let presentationController = vc.presentationController as? UISheetPresentationController { - presentationController.detents = [.large()] - } - self.viewController = vc - window.rootViewController?.present(vc, animated: true, completion: { - completion?(true) - }) - } else { + + guard let window = UIApplication.safeKeyWindow, + let rootVC = window.rootViewController else { + Formbricks.logger?.error("Survey present aborted: no key window or root view controller available.") completion?(false) + return } + + let presenter = self.topMostViewController(from: rootVC) + + // UIAlertController/action-sheets/popovers cannot host a modal sheet — presenting on them either + // crops the survey to the alert frame or is rejected by UIKit. Bail with a clear log so the host + // app can dismiss the alert before triggering the survey. + if presenter is UIAlertController { + Formbricks.logger?.warning("Survey present aborted: top-most VC is a UIAlertController. Dismiss it before triggering the survey.") + completion?(false) + return + } + + let view = FormbricksView(viewModel: FormbricksViewModel(environmentResponse: environmentResponse, surveyId: id)) + let vc = UIHostingController(rootView: view) + vc.modalPresentationStyle = .pageSheet + vc.view.backgroundColor = .clear + if let sheet = vc.sheetPresentationController { + sheet.detents = [.large()] + } + self.viewController = vc + presenter.present(vc, animated: true, completion: { + completion?(true) + }) } } - + /// Dismiss the webview func dismissView() { viewController?.dismiss(animated: true) } - + deinit { Formbricks.logger?.debug("Deinitializing \(self)") }