diff --git a/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift b/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift index c8e3426..327da99 100644 --- a/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift +++ b/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift @@ -65,11 +65,9 @@ final class PresentSurveyManager { let view = FormbricksView( viewModel: FormbricksViewModel(workspaceResponse: workspaceResponse, surveyId: id)) let vc = UIHostingController(rootView: view) - vc.modalPresentationStyle = .pageSheet + vc.modalPresentationStyle = .overFullScreen + vc.modalTransitionStyle = .crossDissolve vc.view.backgroundColor = .clear - if let sheet = vc.sheetPresentationController { - sheet.detents = [.large()] - } self.viewController = vc presenter.present( vc, animated: true, diff --git a/Sources/FormbricksSDK/Manager/SurveyManager.swift b/Sources/FormbricksSDK/Manager/SurveyManager.swift index 907cb5f..face2c1 100644 --- a/Sources/FormbricksSDK/Manager/SurveyManager.swift +++ b/Sources/FormbricksSDK/Manager/SurveyManager.swift @@ -66,8 +66,10 @@ final class SurveyManager { return true } - // Include surveys with segments but no filters - return segment.filters.isEmpty + // Include surveys with segments but no filters. `hasFilters` + // is decoded directly from the server response, or derived + // from a legacy cached `filters` array (see Segment decoder). + return !segment.hasFilters } } @@ -101,7 +103,7 @@ final class SurveyManager { // Display percentage let shouldDisplay = shouldDisplayBasedOnPercentage(firstSurveyWithActionClass?.displayPercentage) if let survey = firstSurveyWithActionClass, !shouldDisplay { - Formbricks.logger?.info("Skipping survey \(survey.name) due to display percentage restriction.") + Formbricks.logger?.info("Skipping survey \(survey.id) due to display percentage restriction.") return } let isMultiLangSurvey = firstSurveyWithActionClass?.languages?.count ?? 0 > 1 @@ -110,7 +112,7 @@ final class SurveyManager { guard let survey = firstSurveyWithActionClass else {return} let currentLanguage = Formbricks.language guard let languageCode = getLanguageCode(survey: survey, language: currentLanguage) else { - Formbricks.logger?.error("Survey \(survey.name) is not available in language “\(currentLanguage)”. Skipping.") + Formbricks.logger?.error("Survey \(survey.id) is not available in language “\(currentLanguage)”. Skipping.") return } @@ -122,7 +124,7 @@ final class SurveyManager { isShowingSurvey = true let timeout = survey.delay ?? 0 if timeout > 0 { - Formbricks.logger?.info("Delaying survey \(survey.name) by \(timeout) seconds") + Formbricks.logger?.info("Delaying survey \(survey.id) by \(timeout) seconds") } DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout)) { [weak self] in guard let self = self else { return } diff --git a/Sources/FormbricksSDK/Model/Workspace/Survey.swift b/Sources/FormbricksSDK/Model/Workspace/Survey.swift index 3544a8d..10f8373 100644 --- a/Sources/FormbricksSDK/Model/Workspace/Survey.swift +++ b/Sources/FormbricksSDK/Model/Workspace/Survey.swift @@ -52,7 +52,9 @@ struct ProjectOverwrites: Codable { struct Survey: Codable { let id: String - let name: String + // `name` intentionally omitted — internal label, not returned by the + // public client API. Cached payloads from older SDK versions may still + // include it in JSON; Codable will quietly ignore unknown keys. let triggers: [Trigger]? let recontactDays: Int? let displayLimit: Int? diff --git a/Sources/FormbricksSDK/Model/Workspace/Surveys/Segment.swift b/Sources/FormbricksSDK/Model/Workspace/Surveys/Segment.swift index 8f94a83..d1d60d7 100644 --- a/Sources/FormbricksSDK/Model/Workspace/Surveys/Segment.swift +++ b/Sources/FormbricksSDK/Model/Workspace/Surveys/Segment.swift @@ -1,232 +1,40 @@ import Foundation -// MARK: - Connector - -enum SegmentConnector: String, Codable { - case and - case or -} - -// MARK: - Filter Operators - -/// Combined operator set for all filter types -enum FilterOperator: String, Codable { - // Base / Arithmetic - case lessThan - case lessEqual - case greaterThan - case greaterEqual - case equals - case notEquals - // Attribute / String - case contains - case doesNotContain - case startsWith - case endsWith - // Existence - case isSet - case isNotSet - // Segment membership - case userIsIn - case userIsNotIn -} - -// MARK: - Filter Value - -enum SegmentFilterValue: Codable { - case string(String) - case number(Double) - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let num = try? container.decode(Double.self) { - self = .number(num) - } else if let str = try? container.decode(String.self) { - self = .string(str) - } else { - throw DecodingError.typeMismatch( - SegmentFilterValue.self, - DecodingError.Context( - codingPath: decoder.codingPath, - debugDescription: "Value is neither Double nor String" - ) - ) - } - } - - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case .number(let num): - try container.encode(num) - case .string(let str): - try container.encode(str) - } - } -} - -// MARK: - Root - -enum SegmentFilterRoot: Codable { - case attribute(contactAttributeKey: String) - case person(personIdentifier: String) - case segment(segmentId: String) - case device(deviceType: String) - - private enum CodingKeys: String, CodingKey { - case type - case contactAttributeKey - case personIdentifier - case segmentId - case deviceType - } - - private enum RootType: String, Codable { - case attribute - case person - case segment - case device - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let type = try container.decode(RootType.self, forKey: .type) - switch type { - case .attribute: - let key = try container.decode(String.self, forKey: .contactAttributeKey) - self = .attribute(contactAttributeKey: key) - case .person: - let id = try container.decode(String.self, forKey: .personIdentifier) - self = .person(personIdentifier: id) - case .segment: - let id = try container.decode(String.self, forKey: .segmentId) - self = .segment(segmentId: id) - case .device: - let type = try container.decode(String.self, forKey: .deviceType) - self = .device(deviceType: type) - } - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - switch self { - case .attribute(let key): - try container.encode(RootType.attribute, forKey: .type) - try container.encode(key, forKey: .contactAttributeKey) - case .person(let id): - try container.encode(RootType.person, forKey: .type) - try container.encode(id, forKey: .personIdentifier) - case .segment(let id): - try container.encode(RootType.segment, forKey: .type) - try container.encode(id, forKey: .segmentId) - case .device(let type): - try container.encode(RootType.device, forKey: .type) - try container.encode(type, forKey: .deviceType) - } - } -} - -// MARK: - Qualifier - -struct SegmentFilterQualifier: Codable { - let filterOperator: FilterOperator - - private enum CodingKeys: String, CodingKey { - case filterOperator = "operator" - } -} - -// MARK: - Primitive Filter - -struct SegmentPrimitiveFilter: Codable { - let id: String - let root: SegmentFilterRoot - let value: SegmentFilterValue - let qualifier: SegmentFilterQualifier - - // Add run-time refinements if needed -} - -// MARK: - Recursive Filter Resource - -enum SegmentFilterResource: Codable { - case primitive(SegmentPrimitiveFilter) - case group([SegmentFilter]) - - init(from decoder: Decoder) throws { - // Try primitive first - if let prim = try? SegmentPrimitiveFilter(from: decoder) { - self = .primitive(prim) - } else { - let nested = try [SegmentFilter](from: decoder) - self = .group(nested) - } - } - - func encode(to encoder: Encoder) throws { - switch self { - case .primitive(let prim): - try prim.encode(to: encoder) - case .group(let arr): - try arr.encode(to: encoder) - } - } -} - -// MARK: - Base Filter (node) - -struct SegmentFilter: Codable { - let id: String - let connector: SegmentConnector? - let resource: SegmentFilterResource -} - // MARK: - Segment Model +/// Public client API returns the minimal `{ id, hasFilters }` shape — full +/// filter logic (titles, descriptions, conditions) is evaluated server-side +/// and must not reach the device. +/// +/// The custom decoder also accepts legacy cached payloads that still carry a +/// `filters` array (written by older SDK versions before the API was slimmed +/// down). In that case `hasFilters` is derived from the array length so +/// anonymous users continue to be excluded from segment-targeted surveys +/// during the cache window after an SDK upgrade. struct Segment: Codable { let id: String - let title: String - let description: String? - let isPrivate: Bool - let filters: [SegmentFilter] - let workspaceId: String? - let createdAt: Date - let updatedAt: Date - let surveys: [String] + let hasFilters: Bool private enum CodingKeys: String, CodingKey { - case id, title, description, filters, surveys, createdAt, updatedAt - case isPrivate = "isPrivate" - case workspaceId - case environmentId + case id, hasFilters, filters } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: .id) - title = try container.decode(String.self, forKey: .title) - description = try container.decodeIfPresent(String.self, forKey: .description) - isPrivate = try container.decode(Bool.self, forKey: .isPrivate) - filters = try container.decode([SegmentFilter].self, forKey: .filters) - createdAt = try container.decode(Date.self, forKey: .createdAt) - updatedAt = try container.decode(Date.self, forKey: .updatedAt) - surveys = try container.decode([String].self, forKey: .surveys) - // Server may send `workspaceId` (new) or `environmentId` (legacy). Field is - // informational only — not read by SDK logic — so keep it optional. - workspaceId = try container.decodeIfPresent(String.self, forKey: .workspaceId) - ?? container.decodeIfPresent(String.self, forKey: .environmentId) + + if let serverHasFilters = try container.decodeIfPresent(Bool.self, forKey: .hasFilters) { + hasFilters = serverHasFilters + } else if let legacyFilters = try container.decodeIfPresent([AnyDecodable].self, forKey: .filters) { + hasFilters = !legacyFilters.isEmpty + } else { + hasFilters = false + } } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) - try container.encode(title, forKey: .title) - try container.encodeIfPresent(description, forKey: .description) - try container.encode(isPrivate, forKey: .isPrivate) - try container.encode(filters, forKey: .filters) - try container.encode(createdAt, forKey: .createdAt) - try container.encode(updatedAt, forKey: .updatedAt) - try container.encode(surveys, forKey: .surveys) - try container.encodeIfPresent(workspaceId, forKey: .workspaceId) + try container.encode(hasFilters, forKey: .hasFilters) } } diff --git a/Sources/FormbricksSDK/WebView/FormbricksViewModel.swift b/Sources/FormbricksSDK/WebView/FormbricksViewModel.swift index 54e52a6..9500c14 100644 --- a/Sources/FormbricksSDK/WebView/FormbricksViewModel.swift +++ b/Sources/FormbricksSDK/WebView/FormbricksViewModel.swift @@ -28,8 +28,8 @@ private extension FormbricksViewModel { Formbricks WebView Survey - -
+ +