From 8d0e249f0361e1fc1cc59d09dc29969364ca0b28 Mon Sep 17 00:00:00 2001 From: pandeymangg Date: Mon, 18 May 2026 12:54:01 +0530 Subject: [PATCH 1/2] simple api response --- .../FormbricksSDK/Manager/SurveyManager.swift | 14 +++++---- .../Model/Environment/Survey.swift | 4 ++- .../Model/Environment/Surveys/Segment.swift | 29 ++++++++++++++----- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/Sources/FormbricksSDK/Manager/SurveyManager.swift b/Sources/FormbricksSDK/Manager/SurveyManager.swift index b01be37a..48211a1d 100644 --- a/Sources/FormbricksSDK/Manager/SurveyManager.swift +++ b/Sources/FormbricksSDK/Manager/SurveyManager.swift @@ -58,9 +58,11 @@ final class SurveyManager { guard let segment = survey.segment else { return true } - - // Include surveys with segments but no filters - return segment.filters.isEmpty + + // Include surveys with segments but no filters. + // `resolvedHasFilters` prefers the server-supplied `hasFilters` + // flag and falls back to the legacy `filters` array. + return !segment.resolvedHasFilters } } @@ -94,7 +96,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 @@ -103,7 +105,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 } @@ -115,7 +117,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/Environment/Survey.swift b/Sources/FormbricksSDK/Model/Environment/Survey.swift index 3544a8d5..08603748 100644 --- a/Sources/FormbricksSDK/Model/Environment/Survey.swift +++ b/Sources/FormbricksSDK/Model/Environment/Survey.swift @@ -52,7 +52,9 @@ struct ProjectOverwrites: Codable { struct Survey: Codable { let id: String - let name: String + // `name` is intentionally optional — the public client API no longer returns it. + // Older cached payloads may still include it, so we accept it when present. + let name: String? let triggers: [Trigger]? let recontactDays: Int? let displayLimit: Int? diff --git a/Sources/FormbricksSDK/Model/Environment/Surveys/Segment.swift b/Sources/FormbricksSDK/Model/Environment/Surveys/Segment.swift index 27436ae4..59573a63 100644 --- a/Sources/FormbricksSDK/Model/Environment/Surveys/Segment.swift +++ b/Sources/FormbricksSDK/Model/Environment/Surveys/Segment.swift @@ -183,20 +183,33 @@ struct SegmentFilter: Codable { // 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. Legacy fields remain optional so cached +/// payloads written by older SDK versions still decode within the cache window. struct Segment: Codable { let id: String - let title: String + let hasFilters: Bool? + let title: String? let description: String? - let isPrivate: Bool - let filters: [SegmentFilter] - let environmentId: String - let createdAt: Date - let updatedAt: Date - let surveys: [String] + let isPrivate: Bool? + let filters: [SegmentFilter]? + let environmentId: String? + let createdAt: Date? + let updatedAt: Date? + let surveys: [String]? private enum CodingKeys: String, CodingKey { - case id, title, description, filters, surveys + case id, hasFilters, title, description, filters, surveys case isPrivate = "isPrivate" case environmentId, createdAt, updatedAt } + + /// Whether this segment has any filters. Prefers the server-supplied + /// `hasFilters` flag; falls back to the legacy `filters` array for + /// payloads cached by older SDK versions. + var resolvedHasFilters: Bool { + if let hasFilters = hasFilters { return hasFilters } + return !(filters?.isEmpty ?? true) + } } From f3a5ecbde1e976018e42b3351728e71859c5c899 Mon Sep 17 00:00:00 2001 From: pandeymangg Date: Mon, 18 May 2026 17:44:40 +0530 Subject: [PATCH 2/2] simple api responses --- .../Manager/PresentSurveyManager.swift | 6 +- .../FormbricksSDK/Manager/SurveyManager.swift | 8 +- .../Model/Workspace/Survey.swift | 6 +- .../Model/Workspace/Surveys/Segment.swift | 242 ++---------------- .../WebView/FormbricksViewModel.swift | 4 +- .../FormbricksSDKTests.swift | 1 - 6 files changed, 29 insertions(+), 238 deletions(-) diff --git a/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift b/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift index c8e34265..327da99d 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 5563a6cd..face2c12 100644 --- a/Sources/FormbricksSDK/Manager/SurveyManager.swift +++ b/Sources/FormbricksSDK/Manager/SurveyManager.swift @@ -66,10 +66,10 @@ final class SurveyManager { return true } - // Include surveys with segments but no filters. - // `resolvedHasFilters` prefers the server-supplied `hasFilters` - // flag and falls back to the legacy `filters` array. - return !segment.resolvedHasFilters + // 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 } } diff --git a/Sources/FormbricksSDK/Model/Workspace/Survey.swift b/Sources/FormbricksSDK/Model/Workspace/Survey.swift index 08603748..10f83739 100644 --- a/Sources/FormbricksSDK/Model/Workspace/Survey.swift +++ b/Sources/FormbricksSDK/Model/Workspace/Survey.swift @@ -52,9 +52,9 @@ struct ProjectOverwrites: Codable { struct Survey: Codable { let id: String - // `name` is intentionally optional — the public client API no longer returns it. - // Older cached payloads may still include it, so we accept it when present. - 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 2682bf52..d1d60d79 100644 --- a/Sources/FormbricksSDK/Model/Workspace/Surveys/Segment.swift +++ b/Sources/FormbricksSDK/Model/Workspace/Surveys/Segment.swift @@ -1,246 +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. Legacy fields remain optional so cached -/// payloads written by older SDK versions still decode within the cache window. +/// 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 hasFilters: Bool? - 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) - } - - /// Whether this segment has any filters. Prefers the server-supplied - /// `hasFilters` flag; falls back to the legacy `filters` array for - /// payloads cached by older SDK versions. - var resolvedHasFilters: Bool { - if let hasFilters = hasFilters { return hasFilters } - return !(filters?.isEmpty ?? true) + try container.encode(hasFilters, forKey: .hasFilters) } } diff --git a/Sources/FormbricksSDK/WebView/FormbricksViewModel.swift b/Sources/FormbricksSDK/WebView/FormbricksViewModel.swift index 54e52a69..9500c14e 100644 --- a/Sources/FormbricksSDK/WebView/FormbricksViewModel.swift +++ b/Sources/FormbricksSDK/WebView/FormbricksViewModel.swift @@ -28,8 +28,8 @@ private extension FormbricksViewModel { Formbricks WebView Survey - -
+ +