Skip to content
Merged
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
18 changes: 4 additions & 14 deletions Sources/FormbricksSDK/Manager/PresentSurveyManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ final class PresentSurveyManager {
private weak var viewController: UIViewController?

/// Present the webview
func present(environmentResponse: EnvironmentResponse, id: String, overlay: SurveyOverlay = .none, completion: ((Bool) -> Void)? = nil) {
/// The native background is always `.clear` — overlay rendering is handled
/// entirely by the JS survey library inside the WebView to avoid double-overlay artifacts.
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 = Self.backgroundColor(for: overlay)
vc.view.backgroundColor = .clear
if let presentationController = vc.presentationController as? UISheetPresentationController {
presentationController.detents = [.large()]
}
Expand All @@ -34,18 +36,6 @@ final class PresentSurveyManager {
}
}

/// Returns the appropriate background color for the given overlay style.
static func backgroundColor(for overlay: SurveyOverlay) -> UIColor {
switch overlay {
case .dark:
return UIColor(white: 0.2, alpha: 0.6)
case .light:
return UIColor(white: 0.6, alpha: 0.4)
case .none:
return .clear
}
}

/// Dismiss the webview
func dismissView() {
viewController?.dismiss(animated: true)
Expand Down
16 changes: 2 additions & 14 deletions Sources/FormbricksSDK/Manager/SurveyManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,7 @@ final class SurveyManager {
DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout)) { [weak self] in
guard let self = self else { return }
if let environmentResponse = self.environmentResponse {
let overlay = self.resolveOverlay(for: survey)
self.presentSurveyManager.present(environmentResponse: environmentResponse, id: survey.id, overlay: overlay) { success in
self.presentSurveyManager.present(environmentResponse: environmentResponse, id: survey.id) { success in
if !success {
self.isShowingSurvey = false
}
Expand Down Expand Up @@ -190,9 +189,7 @@ private extension SurveyManager {
/// The view controller is presented over the current context.
func showSurvey(withId id: String) {
if let environmentResponse = environmentResponse {
let survey = environmentResponse.data.data.surveys?.first(where: { $0.id == id })
let overlay = resolveOverlay(for: survey)
presentSurveyManager.present(environmentResponse: environmentResponse, id: id, overlay: overlay)
presentSurveyManager.present(environmentResponse: environmentResponse, id: id)
}
}

Expand Down Expand Up @@ -347,15 +344,6 @@ extension SurveyManager {
return entry.language.code
}

/// Resolves the overlay style for the given survey, falling back to the project-level default.
/// Survey-level `projectOverwrites.overlay` takes precedence over `project.overlay`.
func resolveOverlay(for survey: Survey?) -> SurveyOverlay {
if let surveyOverlay = survey?.projectOverwrites?.overlay {
return surveyOverlay
}
return environmentResponse?.data.data.project.overlay ?? .none
}

/// Filters the surveys based on the user's segments.
func filterSurveysBasedOnSegments(_ surveys: [Survey], segments: [String]) -> [Survey] {
return surveys.filter { survey in
Expand Down
127 changes: 9 additions & 118 deletions Tests/FormbricksSDKTests/FormbricksSDKTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -261,34 +261,11 @@ final class FormbricksSDKTests: XCTestCase {
XCTAssertNil(manager.getLanguageCode(survey: survey, language: "spanish"))
}

// MARK: - PresentSurveyManager overlay background color tests
// MARK: - PresentSurveyManager tests

func testBackgroundColorForDarkOverlay() {
let color = PresentSurveyManager.backgroundColor(for: .dark)
var white: CGFloat = 0
var alpha: CGFloat = 0
color.getWhite(&white, alpha: &alpha)
XCTAssertEqual(white, 0.2, accuracy: 0.01, "Dark overlay should use 0.2 white")
XCTAssertEqual(alpha, 0.6, accuracy: 0.01, "Dark overlay should use 0.6 alpha")
}

func testBackgroundColorForLightOverlay() {
let color = PresentSurveyManager.backgroundColor(for: .light)
var white: CGFloat = 0
var alpha: CGFloat = 0
color.getWhite(&white, alpha: &alpha)
XCTAssertEqual(white, 0.6, accuracy: 0.01, "Light overlay should use 0.6 white")
XCTAssertEqual(alpha, 0.4, accuracy: 0.01, "Light overlay should use 0.4 alpha")
}

func testBackgroundColorForNoneOverlay() {
let color = PresentSurveyManager.backgroundColor(for: .none)
XCTAssertEqual(color, .clear, "None overlay should return clear color")
}

func testPresentWithOverlayCompletesInHeadlessEnvironment() {
func testPresentCompletesInHeadlessEnvironment() {
// In a headless test environment there is no key window, so present() should
// call the completion with false for every overlay variant.
// call the completion with false.
let config = FormbricksConfig.Builder(appUrl: appUrl, environmentId: environmentId)
.setLogLevel(.debug)
.service(mockService)
Expand All @@ -306,99 +283,13 @@ final class FormbricksSDKTests: XCTestCase {
}

let manager = PresentSurveyManager()

for overlay in [SurveyOverlay.none, .light, .dark] {
let presentExpectation = expectation(description: "Present with \(overlay.rawValue) overlay")
manager.present(environmentResponse: env, id: surveyID, overlay: overlay) { success in
// No key window in headless tests → completion(false)
XCTAssertFalse(success, "Presentation should fail in headless environment for overlay: \(overlay.rawValue)")
presentExpectation.fulfill()
}
wait(for: [presentExpectation], timeout: 2.0)
}
}

// MARK: - SurveyManager.resolveOverlay tests

func testResolveOverlayUsesSurveyOverwrite() {
let config = FormbricksConfig.Builder(appUrl: appUrl, environmentId: environmentId)
.setLogLevel(.debug)
.service(mockService)
.build()
Formbricks.setup(with: config)

Formbricks.surveyManager?.refreshEnvironmentIfNeeded(force: true)
let loadExpectation = expectation(description: "Env loaded")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { loadExpectation.fulfill() }
wait(for: [loadExpectation])

guard let manager = Formbricks.surveyManager else {
XCTFail("Missing surveyManager")
return
}

// The mock survey has projectOverwrites.overlay = "dark"
let surveyWithOverwrite = manager.environmentResponse?.data.data.surveys?.first(where: { $0.id == surveyID })
XCTAssertNotNil(surveyWithOverwrite)
XCTAssertEqual(manager.resolveOverlay(for: surveyWithOverwrite), .dark,
"Should use survey-level projectOverwrites overlay")
}

func testResolveOverlayFallsBackToProjectDefault() {
let config = FormbricksConfig.Builder(appUrl: appUrl, environmentId: environmentId)
.setLogLevel(.debug)
.service(mockService)
.build()
Formbricks.setup(with: config)

Formbricks.surveyManager?.refreshEnvironmentIfNeeded(force: true)
let loadExpectation = expectation(description: "Env loaded")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { loadExpectation.fulfill() }
wait(for: [loadExpectation])

guard let manager = Formbricks.surveyManager else {
XCTFail("Missing surveyManager")
return
let presentExpectation = expectation(description: "Present completes")
manager.present(environmentResponse: env, id: surveyID) { success in
// No key window in headless tests → completion(false)
XCTAssertFalse(success, "Presentation should fail in headless environment")
presentExpectation.fulfill()
}

// A survey without projectOverwrites should fall back to project.overlay ("none" in mock)
let surveyWithoutOverwrite = Survey(
id: "no-overwrite",
name: "No Overwrite",
triggers: nil,
recontactDays: nil,
displayLimit: nil,
delay: nil,
displayPercentage: nil,
displayOption: .respondMultiple,
segment: nil,
styling: nil,
languages: nil,
projectOverwrites: nil
)
XCTAssertEqual(manager.resolveOverlay(for: surveyWithoutOverwrite), .none,
"Should fall back to project-level overlay when no survey overwrite exists")
}

func testResolveOverlayReturnsNoneForNilSurvey() {
let config = FormbricksConfig.Builder(appUrl: appUrl, environmentId: environmentId)
.setLogLevel(.debug)
.service(mockService)
.build()
Formbricks.setup(with: config)

Formbricks.surveyManager?.refreshEnvironmentIfNeeded(force: true)
let loadExpectation = expectation(description: "Env loaded")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { loadExpectation.fulfill() }
wait(for: [loadExpectation])

guard let manager = Formbricks.surveyManager else {
XCTFail("Missing surveyManager")
return
}

XCTAssertEqual(manager.resolveOverlay(for: nil), .none,
"Should return .none when survey is nil")
wait(for: [presentExpectation], timeout: 2.0)
}

// MARK: - WebView data tests
Expand Down