diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index c56c9da2..20c68a66 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -25,6 +25,14 @@ jobs: with: fetch-depth: 0 # Shallow clones should be disabled for better SonarQube analysis + - name: Select Xcode + uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0 + with: + xcode-version: latest-stable + + - name: Show available simulators + run: xcrun simctl list devices available + - name: Install Dependencies run: swift package resolve @@ -34,7 +42,7 @@ jobs: -scheme 'FormbricksSDK' \ -sdk iphonesimulator \ -config Debug \ - -destination 'platform=iOS Simulator,name=iPhone SE (3rd generation),OS=18.5' \ + -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \ -derivedDataPath build \ -enableCodeCoverage YES diff --git a/Sources/FormbricksSDK/Formbricks.swift b/Sources/FormbricksSDK/Formbricks.swift index d1a5315c..0a30b161 100644 --- a/Sources/FormbricksSDK/Formbricks.swift +++ b/Sources/FormbricksSDK/Formbricks.swift @@ -5,7 +5,13 @@ import Network @objc(Formbricks) public class Formbricks: NSObject { static internal var appUrl: String? - static internal var environmentId: String? + static internal var workspaceId: String? + /// Backward-compatible alias for `workspaceId`. + @available(*, deprecated, renamed: "workspaceId", message: "Use workspaceId instead. environmentId will be removed in a future version.") + static internal var environmentId: String? { + get { workspaceId } + set { workspaceId = newValue } + } static internal var language: String = "default" static internal var isInitialized: Bool = false @@ -30,31 +36,35 @@ import Network Example: ```swift - let config = FormbricksConfig.Builder(appUrl: "APP_URL_HERE", environmentId: "TOKEN_HERE") + let config = FormbricksConfig.Builder(appUrl: "APP_URL_HERE", workspaceId: "TOKEN_HERE") .setUserId("USER_ID_HERE") .setLogLevel(.debug) .build() - + Formbricks.setup(with: config) ``` */ @objc public static func setup(with config: FormbricksConfig, force: Bool = false) { logger = Logger() apiQueue = OperationQueue() - + if force { isInitialized = false } - + guard !isInitialized else { let error = FormbricksSDKError(type: .sdkIsAlreadyInitialized) Formbricks.logger?.error(error.message) return } - + self.appUrl = config.appUrl - self.environmentId = config.environmentId + self.workspaceId = config.workspaceId self.logger?.logLevel = config.logLevel + + if config.usedDeprecatedEnvironmentId { + Formbricks.logger?.debug("environmentId is deprecated and will be removed in a future version. Please use workspaceId instead.") + } // Validate appUrl before proceeding with setup guard let url = URL(string: config.appUrl) else { @@ -63,11 +73,11 @@ import Network } // Validate that appUrl uses HTTPS (block HTTP for security) - guard url.scheme?.lowercased() == "https" else { - let errorMessage = "HTTP requests are blocked for security. Only HTTPS URLs are allowed. Provided app url: \(config.appUrl). SDK setup aborted." - Formbricks.logger?.error(errorMessage) - return - } + guard url.scheme?.lowercased() == "https" else { + let errorMessage = "HTTP requests are blocked for security. Only HTTPS URLs are allowed. Provided app url: \(config.appUrl). SDK setup aborted." + Formbricks.logger?.error(errorMessage) + return + } let svc: FormbricksServiceProtocol = config.customService ?? FormbricksService() @@ -88,7 +98,7 @@ import Network surveyManager = SurveyManager.create(userManager: userManager!, presentSurveyManager: presentSurveyManager!, service: svc) userManager?.surveyManager = surveyManager - surveyManager?.refreshEnvironmentIfNeeded(force: force) + surveyManager?.refreshWorkspaceIfNeeded(force: force) userManager?.syncUserStateIfNeeded() self.isInitialized = true @@ -289,7 +299,7 @@ import Network } /** - Cleans up the SDK. This will clear the user attributes, the user id and the environment state. + Cleans up the SDK. This will clear the user attributes, the user id and the workspace state. The SDK must be initialized before calling this method. If `waitForOperations` is set to `true`, it will wait for all operations to finish before cleaning up. If `waitForOperations` is set to `false`, it will clean up immediately. @@ -331,7 +341,7 @@ import Network apiQueue = nil isInitialized = false appUrl = nil - environmentId = nil + workspaceId = nil logger = nil language = "default" } diff --git a/Sources/FormbricksSDK/Helpers/ConfigBuilder.swift b/Sources/FormbricksSDK/Helpers/ConfigBuilder.swift index 28df5356..67efe1e9 100644 --- a/Sources/FormbricksSDK/Helpers/ConfigBuilder.swift +++ b/Sources/FormbricksSDK/Helpers/ConfigBuilder.swift @@ -3,43 +3,61 @@ import Foundation /// The configuration object for the Formbricks SDK. @objc(FormbricksConfig) public class FormbricksConfig: NSObject { let appUrl: String - let environmentId: String + @objc public let workspaceId: String let userId: String? let attributes: [String: AttributeValue]? let logLevel: LogLevel /// Optional custom service, injected via Builder let customService: FormbricksServiceProtocol? - - init(appUrl: String, environmentId: String, userId: String?, attributes: [String: AttributeValue]?, logLevel: LogLevel, customService: FormbricksServiceProtocol?) { + /// True if this config was built using the deprecated `environmentId` parameter. + let usedDeprecatedEnvironmentId: Bool + + /// Backward-compatible alias for `workspaceId`. + @available(*, deprecated, renamed: "workspaceId", message: "Use workspaceId instead. environmentId will be removed in a future version.") + @objc public var environmentId: String { workspaceId } + + init(appUrl: String, workspaceId: String, userId: String?, attributes: [String: AttributeValue]?, logLevel: LogLevel, customService: FormbricksServiceProtocol?, usedDeprecatedEnvironmentId: Bool = false) { self.appUrl = appUrl - self.environmentId = environmentId + self.workspaceId = workspaceId self.userId = userId self.attributes = attributes self.logLevel = logLevel self.customService = customService + self.usedDeprecatedEnvironmentId = usedDeprecatedEnvironmentId } - + /// The builder class for the FormbricksConfig object. @objc(FormbricksConfigBuilder) public class Builder: NSObject { var appUrl: String - var environmentId: String + var workspaceId: String var userId: String? var attributes: [String: AttributeValue] = [:] var logLevel: LogLevel = .error /// Optional custom service, injected via Builder var customService: FormbricksServiceProtocol? - + var usedDeprecatedEnvironmentId: Bool = false + + /// Initializes the builder with the workspace ID. + @objc public init(appUrl: String, workspaceId: String) { + self.appUrl = appUrl + self.workspaceId = workspaceId + } + + /// Initializes the builder with the environment ID. + /// - Warning: `environmentId` is deprecated — use `init(appUrl:workspaceId:)` instead. + @available(*, deprecated, renamed: "init(appUrl:workspaceId:)", message: "Use init(appUrl:workspaceId:) instead. environmentId will be removed in a future version.") @objc public init(appUrl: String, environmentId: String) { self.appUrl = appUrl - self.environmentId = environmentId + self.workspaceId = environmentId + self.usedDeprecatedEnvironmentId = true } - + /// Sets the user id for the Builder object. @objc public func set(userId: String) -> Builder { self.userId = userId return self } - + /// Sets the attributes for the Builder object. /// /// Thanks to `ExpressibleByStringLiteral`, `ExpressibleByIntegerLiteral`, @@ -58,27 +76,27 @@ import Foundation self.attributes = stringAttributes.mapValues { .string($0) } return self } - + /// Adds a string attribute to the Builder object (Obj-C compatible). @objc public func add(attribute: String, forKey key: String) -> Builder { self.attributes[key] = .string(attribute) return self } - + /// Sets the log level for the Builder object. @objc public func setLogLevel(_ logLevel: LogLevel) -> Builder { self.logLevel = logLevel return self } - + func service(_ svc: FormbricksServiceProtocol) -> FormbricksConfig.Builder { self.customService = svc return self } - + /// Builds the FormbricksConfig object from the Builder object. @objc public func build() -> FormbricksConfig { - return FormbricksConfig(appUrl: appUrl, environmentId: environmentId, userId: userId, attributes: attributes, logLevel: logLevel, customService: customService) + return FormbricksConfig(appUrl: appUrl, workspaceId: workspaceId, userId: userId, attributes: attributes, logLevel: logLevel, customService: customService, usedDeprecatedEnvironmentId: usedDeprecatedEnvironmentId) } } } diff --git a/Sources/FormbricksSDK/Helpers/FormbricksEnvironment.swift b/Sources/FormbricksSDK/Helpers/FormbricksEnvironment.swift deleted file mode 100644 index dc2974a3..00000000 --- a/Sources/FormbricksSDK/Helpers/FormbricksEnvironment.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation - -internal enum FormbricksEnvironment { - - /// Only `appUrl` is user-supplied. Returns nil if it's missing. - internal static var baseApiUrl: String? { - return Formbricks.appUrl - } - - /// Returns the full survey‐script URL as a String - static var surveyScriptUrlString: String? { - guard let baseURLString = baseApiUrl, - let baseURL = URL(string: baseURLString), - baseURL.scheme == "https" || baseURL.scheme == "http" else { - return nil - } - let surveyScriptURL = baseURL.appendingPathComponent("js").appendingPathComponent("surveys.umd.cjs") - return surveyScriptURL.absoluteString - } - - /// Returns the full environment‐fetch URL as a String for the given ID - static var getEnvironmentRequestEndpoint: String { - let path = ["api", "v2", "client", "{environmentId}", "environment"].joined(separator: "/") - return path - } - - /// Returns the full post-user URL as a String for the given ID - static var postUserRequestEndpoint: String { - return ["api", "v2", "client", "{environmentId}", "user"].joined(separator: "/") - } -} diff --git a/Sources/FormbricksSDK/Helpers/FormbricksWorkspace.swift b/Sources/FormbricksSDK/Helpers/FormbricksWorkspace.swift new file mode 100644 index 00000000..e2de83df --- /dev/null +++ b/Sources/FormbricksSDK/Helpers/FormbricksWorkspace.swift @@ -0,0 +1,32 @@ +import Foundation + +internal enum FormbricksWorkspace { + + /// Only `appUrl` is user-supplied. Returns nil if it's missing. + internal static var baseApiUrl: String? { + return Formbricks.appUrl + } + + /// Returns the full survey‐script URL as a String + static var surveyScriptUrlString: String? { + guard let baseURLString = baseApiUrl, + let baseURL = URL(string: baseURLString), + baseURL.scheme == "https" || baseURL.scheme == "http" + else { + return nil + } + let surveyScriptURL = baseURL.appendingPathComponent("js").appendingPathComponent( + "surveys.umd.cjs") + return surveyScriptURL.absoluteString + } + + /// Returns the workspace-state fetch URL path with a `{workspaceId}` placeholder. + static var getWorkspaceStateRequestEndpoint: String { + return ["api", "v2", "client", "{workspaceId}", "environment"].joined(separator: "/") + } + + /// Returns the post-user URL path with a `{workspaceId}` placeholder. + static var postUserRequestEndpoint: String { + return ["api", "v2", "client", "{workspaceId}", "user"].joined(separator: "/") + } +} diff --git a/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift b/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift index fc22e72d..c8e34265 100644 --- a/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift +++ b/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift @@ -16,28 +16,35 @@ final class PresentSurveyManager { /// 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 { + !presented.isBeingDismissed + { return topMostViewController(from: presented) } if let navigation = viewController as? UINavigationController, - let visible = navigation.visibleViewController { + let visible = navigation.visibleViewController + { return topMostViewController(from: visible) } if let tabBar = viewController as? UITabBarController, - let selected = tabBar.selectedViewController { + 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) { + func present( + workspaceResponse: WorkspaceResponse, id: String, completion: ((Bool) -> Void)? = nil + ) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } guard let window = UIApplication.safeKeyWindow, - let rootVC = window.rootViewController else { - Formbricks.logger?.error("Survey present aborted: no key window or root view controller available.") + let rootVC = window.rootViewController + else { + Formbricks.logger?.error( + "Survey present aborted: no key window or root view controller available.") completion?(false) return } @@ -48,12 +55,15 @@ final class PresentSurveyManager { // 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.") + 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 view = FormbricksView( + viewModel: FormbricksViewModel(workspaceResponse: workspaceResponse, surveyId: id)) let vc = UIHostingController(rootView: view) vc.modalPresentationStyle = .pageSheet vc.view.backgroundColor = .clear @@ -61,9 +71,11 @@ final class PresentSurveyManager { sheet.detents = [.large()] } self.viewController = vc - presenter.present(vc, animated: true, completion: { - completion?(true) - }) + presenter.present( + vc, animated: true, + completion: { + completion?(true) + }) } } diff --git a/Sources/FormbricksSDK/Manager/SurveyManager.swift b/Sources/FormbricksSDK/Manager/SurveyManager.swift index b01be37a..907cb5ff 100644 --- a/Sources/FormbricksSDK/Manager/SurveyManager.swift +++ b/Sources/FormbricksSDK/Manager/SurveyManager.swift @@ -1,6 +1,11 @@ import SwiftUI public extension Notification.Name { + /// Posted when the workspace state has been refreshed. + static let workspaceRefreshed = Notification.Name("Formbricks.workspaceRefreshed") + + /// Backward-compatible alias for `workspaceRefreshed`. The SDK posts both names. + @available(*, deprecated, renamed: "workspaceRefreshed", message: "Use .workspaceRefreshed instead. environmentRefreshed will be removed in a future version.") static let environmentRefreshed = Notification.Name("Formbricks.environmentRefreshed") } @@ -17,7 +22,7 @@ final class SurveyManager { self.presentSurveyManager = presentSurveyManager self.service = service } - + static func create( userManager: UserManager, presentSurveyManager: PresentSurveyManager, @@ -29,28 +34,30 @@ final class SurveyManager { service: service ) } - - private static let environmentResponseObjectKey = "environmentResponseObjectKey" - private var backingEnvironmentResponse: EnvironmentResponse? + + internal static let workspaceResponseObjectKey = "workspaceResponseObjectKey" + /// Pre-workspace-rename storage key. Read on first access so existing installs can be migrated. + internal static let legacyEnvironmentResponseObjectKey = "environmentResponseObjectKey" + private var backingWorkspaceResponse: WorkspaceResponse? /// Stores the surveys that are filtered based on the defined criteria, such as recontact days, display options etc. internal private(set) var filteredSurveys: [Survey] = [] /// Stores is a survey is being shown or the show in delayed internal private(set) var isShowingSurvey: Bool = false /// Store error state internal private(set) var hasApiError: Bool = false - + /// Fills up the `filteredSurveys` array func filterSurveys() { - guard let environment = environmentResponse else { return } - guard let surveys = environment.data.data.surveys else { return } - + guard let workspace = workspaceResponse else { return } + guard let surveys = workspace.data.data.surveys else { return } + let displays = userManager.displays ?? [] let responses = userManager.responses ?? [] let segments = userManager.segments ?? [] - + filteredSurveys = filterSurveysBasedOnDisplayType(surveys, displays: displays, responses: responses) - filteredSurveys = filterSurveysBasedOnRecontactDays(filteredSurveys, defaultRecontactDays: environment.data.data.project.recontactDays) - + filteredSurveys = filterSurveysBasedOnRecontactDays(filteredSurveys, defaultRecontactDays: workspace.data.data.settings.recontactDays) + // If we don't have a user, we exclude surveys that have segments with filters if userManager.userId == nil { filteredSurveys = filteredSurveys.filter { survey in @@ -58,39 +65,39 @@ final class SurveyManager { guard let segment = survey.segment else { return true } - + // Include surveys with segments but no filters return segment.filters.isEmpty } } - + // If we have a user, we do more filtering if userManager.userId != nil { if segments.isEmpty { filteredSurveys = [] return } - + filteredSurveys = filterSurveysBasedOnSegments(filteredSurveys, segments: segments) } } - + /// Checks if there are any surveys to display, based in the track action, and if so, displays the first one. /// Handles the display percentage and the delay of the survey. func track(_ action: String, completion: (() -> Void)? = nil) { guard !isShowingSurvey else { return } - - let actionClasses = environmentResponse?.data.data.actionClasses ?? [] + + let actionClasses = workspaceResponse?.data.data.actionClasses ?? [] let codeActionClasses = actionClasses.filter { $0.type == "code" } guard let actionClass = codeActionClasses.first(where: { $0.key == action }) else { Formbricks.logger?.error("Action with identifier '\(action)' is unknown. Please add this action in Formbricks in order to use it via the SDK action tracking.") return } - + let firstSurveyWithActionClass = filteredSurveys.first { survey in return survey.triggers?.contains(where: { $0.actionClass?.name == actionClass.name }) ?? false } - + // Display percentage let shouldDisplay = shouldDisplayBasedOnPercentage(firstSurveyWithActionClass?.displayPercentage) if let survey = firstSurveyWithActionClass, !shouldDisplay { @@ -106,7 +113,7 @@ final class SurveyManager { Formbricks.logger?.error("Survey \(survey.name) is not available in language “\(currentLanguage)”. Skipping.") return } - + Formbricks.language = languageCode } @@ -119,8 +126,8 @@ final class SurveyManager { } DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout)) { [weak self] in guard let self = self else { return } - if let environmentResponse = self.environmentResponse { - self.presentSurveyManager.present(environmentResponse: environmentResponse, id: survey.id) { success in + if let workspaceResponse = self.workspaceResponse { + self.presentSurveyManager.present(workspaceResponse: workspaceResponse, id: survey.id) { success in if !success { self.isShowingSurvey = false } @@ -137,37 +144,38 @@ final class SurveyManager { // MARK: - API calls - extension SurveyManager { - /// Checks if the environment state needs to be refreshed based on its `expiresAt` property, and if so, refreshes it, starts the refresh timer, and filters the surveys. - func refreshEnvironmentIfNeeded(force: Bool = false) { - if let environmentResponse = environmentResponse, environmentResponse.data.expiresAt.timeIntervalSinceNow > 0, !force { - Formbricks.logger?.debug("Environment state is still valid until \(environmentResponse.data.expiresAt)") + /// Checks if the workspace state needs to be refreshed based on its `expiresAt` property, and if so, refreshes it, starts the refresh timer, and filters the surveys. + func refreshWorkspaceIfNeeded(force: Bool = false) { + if let workspaceResponse = workspaceResponse, workspaceResponse.data.expiresAt.timeIntervalSinceNow > 0, !force { + Formbricks.logger?.debug("Workspace state is still valid until \(workspaceResponse.data.expiresAt)") filterSurveys() + startRefreshTimer(expiresAt: workspaceResponse.data.expiresAt) return } - - service.getEnvironmentState { [weak self] result in + + service.getWorkspaceState { [weak self] result in switch result { case .success(let response): self?.hasApiError = false - self?.environmentResponse = response + self?.workspaceResponse = response self?.startRefreshTimer(expiresAt: response.data.expiresAt) self?.filterSurveys() - NotificationCenter.default.post(name: .environmentRefreshed, object: self) + SurveyManager.postWorkspaceRefreshed(object: self) case .failure: self?.hasApiError = true let error = FormbricksSDKError(type: .unableToRefreshEnvironment) Formbricks.logger?.error(error.message) self?.startErrorTimer() - NotificationCenter.default.post(name: .environmentRefreshed, object: self) + SurveyManager.postWorkspaceRefreshed(object: self) } } } - + /// Posts a survey response to the Formbricks API. func postResponse(surveyId: String) { userManager.onResponse(surveyId: surveyId) } - + /// Creates a new display for the survey. It is called when the survey is displayed to the user. func onNewDisplay(surveyId: String) { userManager.onDisplay(surveyId: surveyId) @@ -188,34 +196,34 @@ private extension SurveyManager { /// The survey is displayed based on the `FormbricksView`. /// The view controller is presented over the current context. func showSurvey(withId id: String) { - if let environmentResponse = environmentResponse { - presentSurveyManager.present(environmentResponse: environmentResponse, id: id) + if let workspaceResponse = workspaceResponse { + presentSurveyManager.present(workspaceResponse: workspaceResponse, id: id) } } - - /// Starts a timer to refresh the environment state after the given timeout (`expiresAt`). + + /// Starts a timer to refresh the workspace state after the given timeout (`expiresAt`). func startRefreshTimer(expiresAt: Date) { let timeout = expiresAt.timeIntervalSinceNow - refreshEnvironmentAfter(timeout: timeout) + refreshWorkspaceAfter(timeout: timeout) } - - /// When an error occurs, it starts a timer to refresh the environment state after the given timeout. + + /// When an error occurs, it starts a timer to refresh the workspace state after the given timeout. func startErrorTimer() { - refreshEnvironmentAfter(timeout: Double(Config.Environment.refreshStateOnErrorTimeoutInMinutes) * 60.0) + refreshWorkspaceAfter(timeout: Double(Config.Environment.refreshStateOnErrorTimeoutInMinutes) * 60.0) } - - /// Refreshes the environment state after the given timeout. - internal func refreshEnvironmentAfter(timeout: Double) { + + /// Refreshes the workspace state after the given timeout. + internal func refreshWorkspaceAfter(timeout: Double) { guard timeout > 0 else { return } - + DispatchQueue.global().asyncAfter(deadline: .now() + timeout) { [weak self] in - Formbricks.logger?.debug("Refreshing environment state.") - self?.refreshEnvironmentIfNeeded(force: true) + Formbricks.logger?.debug("Refreshing workspace state.") + self?.refreshWorkspaceIfNeeded(force: true) } } - + /// Decides if the survey should be displayed based on the display percentage. internal func shouldDisplayBasedOnPercentage(_ displayPercentage: Double?) -> Bool { guard let displayPercentage = displayPercentage else { return true } @@ -223,27 +231,49 @@ private extension SurveyManager { let draw = Double.random(in: 0..<100) return draw < clampedPercentage } + + /// Posts both `.workspaceRefreshed` (new) and `.environmentRefreshed` (legacy alias) + /// so existing subscribers keep working after the rename. + static func postWorkspaceRefreshed(object: Any?) { + NotificationCenter.default.post(name: .workspaceRefreshed, object: object) + NotificationCenter.default.post(name: Notification.Name("Formbricks.environmentRefreshed"), object: object) + } } // MARK: - Store data in the UserDefaults - extension SurveyManager { - var environmentResponse: EnvironmentResponse? { + var workspaceResponse: WorkspaceResponse? { get { - if let environmentResponse = backingEnvironmentResponse { - return environmentResponse - } else { - if let data = UserDefaults.standard.data(forKey: SurveyManager.environmentResponseObjectKey) { - return try? JSONDecoder().decode(EnvironmentResponse.self, from: data) - } else { - let error = FormbricksSDKError(type: .unableToRetrieveEnvironment) - Formbricks.logger?.error(error.message) - return nil - } + if let workspaceResponse = backingWorkspaceResponse { + return workspaceResponse + } + + // Prefer the new key; fall back to the legacy key for installs that still + // have data stored under the pre-rename `environmentResponseObjectKey`. + let defaults = UserDefaults.standard + if let data = defaults.data(forKey: SurveyManager.workspaceResponseObjectKey), + let decoded = try? JSONDecoder().decode(WorkspaceResponse.self, from: data) { + return decoded } + + if let legacyData = defaults.data(forKey: SurveyManager.legacyEnvironmentResponseObjectKey), + let decoded = try? JSONDecoder().decode(WorkspaceResponse.self, from: legacyData) { + // Only migrate after a successful decode, so a corrupt legacy blob + // can't poison the new key or get silently discarded. + defaults.set(legacyData, forKey: SurveyManager.workspaceResponseObjectKey) + defaults.removeObject(forKey: SurveyManager.legacyEnvironmentResponseObjectKey) + return decoded + } + + let error = FormbricksSDKError(type: .unableToRetrieveEnvironment) + Formbricks.logger?.error(error.message) + return nil } set { if let data = try? JSONEncoder().encode(newValue) { - UserDefaults.standard.set(data, forKey: SurveyManager.environmentResponseObjectKey) - backingEnvironmentResponse = newValue + UserDefaults.standard.set(data, forKey: SurveyManager.workspaceResponseObjectKey) + // Drop the legacy cache key once we've written to the new one. + UserDefaults.standard.removeObject(forKey: SurveyManager.legacyEnvironmentResponseObjectKey) + backingWorkspaceResponse = newValue } else { let error = FormbricksSDKError(type: .unableToPersistEnvironment) Formbricks.logger?.error(error.message) @@ -260,13 +290,13 @@ extension SurveyManager { switch survey.displayOption { case .respondMultiple: return true - + case .displayOnce: return !displays.contains { $0.surveyId == survey.id } - + case .displayMultiple: return !responses.contains { $0 == survey.id } - + case .displaySome: if let limit = survey.displayLimit { if responses.contains(where: { $0 == survey.id }) { @@ -276,33 +306,33 @@ extension SurveyManager { } else { return true } - + default: let error = FormbricksSDKError(type: .invalidDisplayOption) Formbricks.logger?.error(error.message) return false } - - + + } } - + /// Filters the surveys based on the recontact days and the `lastDisplayedAt` date. func filterSurveysBasedOnRecontactDays(_ surveys: [Survey], defaultRecontactDays: Int?) -> [Survey] { surveys.filter { survey in guard let lastDisplayedAt = userManager.lastDisplayedAt else { return true } let recontactDays = survey.recontactDays ?? defaultRecontactDays - + if let recontactDays = recontactDays { let secondsElapsed = Date().timeIntervalSince(lastDisplayedAt) let daysBetween = Int(secondsElapsed / 86_400) return daysBetween >= recontactDays } - + return true } } - + internal func getLanguageCode( survey: Survey, language: String? @@ -310,27 +340,27 @@ extension SurveyManager { // 1) Collect all codes let availableLanguageCodes = survey.languages? .map { $0.language.code } - + // 2) If no language was passed or it's the explicit "default" token → default guard let raw = language?.lowercased(), !raw.isEmpty else { return "default" } - + if raw == "default" { return "default" } - + // 3) Find matching entry by code or alias let selected = survey.languages?.first { entry in entry.language.code.lowercased() == raw || entry.language.alias?.lowercased() == raw } - + // 4) If that entry is marked default → default if selected?.isDefault == true { return "default" } - + // 5) If no entry, or not enabled, or code not in the available list → nil guard let entry = selected, @@ -339,11 +369,11 @@ extension SurveyManager { else { return nil } - + // 6) Otherwise return its code return entry.language.code } - + /// Filters the surveys based on the user's segments. func filterSurveysBasedOnSegments(_ surveys: [Survey], segments: [String]) -> [Survey] { return surveys.filter { survey in diff --git a/Sources/FormbricksSDK/Model/Environment/EnvironmentData.swift b/Sources/FormbricksSDK/Model/Environment/EnvironmentData.swift deleted file mode 100644 index 47407f79..00000000 --- a/Sources/FormbricksSDK/Model/Environment/EnvironmentData.swift +++ /dev/null @@ -1,6 +0,0 @@ -struct EnvironmentData: Codable { - let surveys: [Survey]? - let actionClasses: [ActionClass]? - let project: Project - let recaptchaSiteKey: String? -} diff --git a/Sources/FormbricksSDK/Model/Environment/EnvironmentResponseData.swift b/Sources/FormbricksSDK/Model/Environment/EnvironmentResponseData.swift deleted file mode 100644 index 507df667..00000000 --- a/Sources/FormbricksSDK/Model/Environment/EnvironmentResponseData.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -struct EnvironmentResponseData: Codable { - let data: EnvironmentData - let expiresAt: Date -} diff --git a/Sources/FormbricksSDK/Model/Environment/ActionClass/ActionClass.swift b/Sources/FormbricksSDK/Model/Workspace/ActionClass/ActionClass.swift similarity index 100% rename from Sources/FormbricksSDK/Model/Environment/ActionClass/ActionClass.swift rename to Sources/FormbricksSDK/Model/Workspace/ActionClass/ActionClass.swift diff --git a/Sources/FormbricksSDK/Model/Environment/Common/LocalizedText.swift b/Sources/FormbricksSDK/Model/Workspace/Common/LocalizedText.swift similarity index 100% rename from Sources/FormbricksSDK/Model/Environment/Common/LocalizedText.swift rename to Sources/FormbricksSDK/Model/Workspace/Common/LocalizedText.swift diff --git a/Sources/FormbricksSDK/Model/Environment/Project/BrandColor.swift b/Sources/FormbricksSDK/Model/Workspace/Settings/BrandColor.swift similarity index 100% rename from Sources/FormbricksSDK/Model/Environment/Project/BrandColor.swift rename to Sources/FormbricksSDK/Model/Workspace/Settings/BrandColor.swift diff --git a/Sources/FormbricksSDK/Model/Environment/Project/Project.swift b/Sources/FormbricksSDK/Model/Workspace/Settings/Settings.swift similarity index 88% rename from Sources/FormbricksSDK/Model/Environment/Project/Project.swift rename to Sources/FormbricksSDK/Model/Workspace/Settings/Settings.swift index cd92c094..cbb4bf58 100644 --- a/Sources/FormbricksSDK/Model/Environment/Project/Project.swift +++ b/Sources/FormbricksSDK/Model/Workspace/Settings/Settings.swift @@ -1,4 +1,4 @@ -struct Project: Codable { +struct Settings: Codable { let id: String? let recontactDays: Int? let clickOutsideClose: Bool? diff --git a/Sources/FormbricksSDK/Model/Environment/Project/Styling.swift b/Sources/FormbricksSDK/Model/Workspace/Settings/Styling.swift similarity index 100% rename from Sources/FormbricksSDK/Model/Environment/Project/Styling.swift rename to Sources/FormbricksSDK/Model/Workspace/Settings/Styling.swift diff --git a/Sources/FormbricksSDK/Model/Environment/Survey.swift b/Sources/FormbricksSDK/Model/Workspace/Survey.swift similarity index 100% rename from Sources/FormbricksSDK/Model/Environment/Survey.swift rename to Sources/FormbricksSDK/Model/Workspace/Survey.swift diff --git a/Sources/FormbricksSDK/Model/Environment/Surveys/ActionClassReference.swift b/Sources/FormbricksSDK/Model/Workspace/Surveys/ActionClassReference.swift similarity index 100% rename from Sources/FormbricksSDK/Model/Environment/Surveys/ActionClassReference.swift rename to Sources/FormbricksSDK/Model/Workspace/Surveys/ActionClassReference.swift diff --git a/Sources/FormbricksSDK/Model/Environment/Surveys/Segment.swift b/Sources/FormbricksSDK/Model/Workspace/Surveys/Segment.swift similarity index 74% rename from Sources/FormbricksSDK/Model/Environment/Surveys/Segment.swift rename to Sources/FormbricksSDK/Model/Workspace/Surveys/Segment.swift index 27436ae4..8f94a837 100644 --- a/Sources/FormbricksSDK/Model/Environment/Surveys/Segment.swift +++ b/Sources/FormbricksSDK/Model/Workspace/Surveys/Segment.swift @@ -189,14 +189,44 @@ struct Segment: Codable { let description: String? let isPrivate: Bool let filters: [SegmentFilter] - let environmentId: String + let workspaceId: String? let createdAt: Date let updatedAt: Date let surveys: [String] private enum CodingKeys: String, CodingKey { - case id, title, description, filters, surveys + case id, title, description, filters, surveys, createdAt, updatedAt case isPrivate = "isPrivate" - case environmentId, createdAt, updatedAt + case workspaceId + case environmentId + } + + 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) + } + + 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) } } diff --git a/Sources/FormbricksSDK/Model/Environment/Surveys/Trigger.swift b/Sources/FormbricksSDK/Model/Workspace/Surveys/Trigger.swift similarity index 100% rename from Sources/FormbricksSDK/Model/Environment/Surveys/Trigger.swift rename to Sources/FormbricksSDK/Model/Workspace/Surveys/Trigger.swift diff --git a/Sources/FormbricksSDK/Model/Workspace/WorkspaceData.swift b/Sources/FormbricksSDK/Model/Workspace/WorkspaceData.swift new file mode 100644 index 00000000..56c86877 --- /dev/null +++ b/Sources/FormbricksSDK/Model/Workspace/WorkspaceData.swift @@ -0,0 +1,47 @@ +struct WorkspaceData: Codable { + let surveys: [Survey]? + let actionClasses: [ActionClass]? + let settings: Settings + let recaptchaSiteKey: String? + + enum CodingKeys: String, CodingKey { + case surveys + case actionClasses + case settings + case workspace + case project + case recaptchaSiteKey + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + surveys = try container.decodeIfPresent([Survey].self, forKey: .surveys) + actionClasses = try container.decodeIfPresent([ActionClass].self, forKey: .actionClasses) + recaptchaSiteKey = try container.decodeIfPresent(String.self, forKey: .recaptchaSiteKey) + + // Server may respond with `settings`, `workspace`, or legacy `project` — all carry the same shape. + if let settings = try container.decodeIfPresent(Settings.self, forKey: .settings) { + self.settings = settings + } else if let workspace = try container.decodeIfPresent(Settings.self, forKey: .workspace) { + self.settings = workspace + } else if let project = try container.decodeIfPresent(Settings.self, forKey: .project) { + self.settings = project + } else { + throw DecodingError.keyNotFound( + CodingKeys.settings, + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Expected one of 'settings', 'workspace', or 'project' key in workspace data" + ) + ) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(surveys, forKey: .surveys) + try container.encodeIfPresent(actionClasses, forKey: .actionClasses) + try container.encodeIfPresent(recaptchaSiteKey, forKey: .recaptchaSiteKey) + try container.encode(settings, forKey: .settings) + } +} diff --git a/Sources/FormbricksSDK/Model/Environment/EnvironmentResponse.swift b/Sources/FormbricksSDK/Model/Workspace/WorkspaceResponse.swift similarity index 76% rename from Sources/FormbricksSDK/Model/Environment/EnvironmentResponse.swift rename to Sources/FormbricksSDK/Model/Workspace/WorkspaceResponse.swift index ce5e8f02..aedcd5a5 100644 --- a/Sources/FormbricksSDK/Model/Environment/EnvironmentResponse.swift +++ b/Sources/FormbricksSDK/Model/Workspace/WorkspaceResponse.swift @@ -1,17 +1,17 @@ import Foundation -struct EnvironmentResponse: Codable { - let data: EnvironmentResponseData - +struct WorkspaceResponse: Codable { + let data: WorkspaceResponseData + var responseString: String? - + enum CodingKeys: CodingKey { case data case responseString } } -extension EnvironmentResponse { +extension WorkspaceResponse { func getSurveyJson(forSurveyId surveyId: String) -> [String: Any]? { guard let jsonData = responseString?.data(using: .utf8) else { return nil } let responseDictionary = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] @@ -20,7 +20,7 @@ extension EnvironmentResponse { let surveysArray = dataDict?["surveys"] as? [[String: Any]] return surveysArray?.first(where: { $0["id"] as? String == surveyId }) as? [String: Any] } - + func getSurveyStylingJson(forSurveyId surveyId: String) -> [String: Any]? { guard let jsonData = responseString?.data(using: .utf8) else { return nil } let responseDictionary = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] @@ -31,12 +31,15 @@ extension EnvironmentResponse { return survey?["styling"] as? [String: Any] } - func getProjectStylingJson() -> [String: Any]? { + func getSettingsStylingJson() -> [String: Any]? { guard let jsonData = responseString?.data(using: .utf8) else { return nil } let responseDictionary = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] let responseDict = responseDictionary?["data"] as? [String: Any] let dataDict = responseDict?["data"] as? [String: Any] - let projectDict = dataDict?["project"] as? [String: Any] - return projectDict?["styling"] as? [String: Any] + // Server may respond with `settings`, `workspace`, or legacy `project` — all carry the same shape. + let settingsDict = (dataDict?["settings"] as? [String: Any]) + ?? (dataDict?["workspace"] as? [String: Any]) + ?? (dataDict?["project"] as? [String: Any]) + return settingsDict?["styling"] as? [String: Any] } } diff --git a/Sources/FormbricksSDK/Model/Workspace/WorkspaceResponseData.swift b/Sources/FormbricksSDK/Model/Workspace/WorkspaceResponseData.swift new file mode 100644 index 00000000..8576bd34 --- /dev/null +++ b/Sources/FormbricksSDK/Model/Workspace/WorkspaceResponseData.swift @@ -0,0 +1,6 @@ +import Foundation + +struct WorkspaceResponseData: Codable { + let data: WorkspaceData + let expiresAt: Date +} diff --git a/Sources/FormbricksSDK/Networking/Base/APIClient.swift b/Sources/FormbricksSDK/Networking/Base/APIClient.swift index 45e1a8a8..5d462312 100644 --- a/Sources/FormbricksSDK/Networking/Base/APIClient.swift +++ b/Sources/FormbricksSDK/Networking/Base/APIClient.swift @@ -30,11 +30,11 @@ class APIClient: Operation, @unchecked Sendable { guard let apiURL = request.baseURL, var components = URLComponents(string: apiURL) else { return nil } // Ensure only HTTPS requests are allowed (block HTTP) - guard let scheme = components.scheme?.lowercased(), scheme == "https" else { - let errorMessage = "HTTP requests are blocked for security. Only HTTPS requests are allowed. Provided app url: \(apiURL)" - Formbricks.logger?.error(errorMessage) - return nil - } + guard let scheme = components.scheme?.lowercased(), scheme == "https" else { + let errorMessage = "HTTP requests are blocked for security. Only HTTPS requests are allowed. Provided app url: \(apiURL)" + Formbricks.logger?.error(errorMessage) + return nil + } components.queryItems = request.queryParams?.map { URLQueryItem(name: $0.key, value: $0.value) } @@ -78,9 +78,9 @@ class APIClient: Operation, @unchecked Sendable { completion?(.success(VoidResponse() as! Request.Response)) } else { var body = try request.decoder.decode(Request.Response.self, from: data) - if var env = body as? EnvironmentResponse, let jsonString = String(data: data, encoding: .utf8) { - env.responseString = jsonString - body = env as! Request.Response + if var workspace = body as? WorkspaceResponse, let jsonString = String(data: data, encoding: .utf8) { + workspace.responseString = jsonString + body = workspace as! Request.Response } Formbricks.logger?.info(message) completion?(.success(body)) @@ -120,7 +120,7 @@ class APIClient: Operation, @unchecked Sendable { default: message.append("Error: \(error.localizedDescription)") } - + let error = FormbricksAPIClientError(type: .invalidResponse, statusCode: statusCode) Formbricks.logger?.error(error.message) completion?(.failure(error)) @@ -162,14 +162,16 @@ private extension APIClient { func setPathParams(_ path: String) -> String? { var newPath = path - if let environmentId = Formbricks.environmentId { - newPath = newPath.replacingOccurrences(of: "{environmentId}", with: environmentId) + if let workspaceId = Formbricks.workspaceId { + newPath = newPath.replacingOccurrences(of: "{workspaceId}", with: workspaceId) + // Backward-compatible: still replace the legacy `{environmentId}` placeholder. + newPath = newPath.replacingOccurrences(of: "{environmentId}", with: workspaceId) } - + request.pathParams?.forEach { key, value in newPath = newPath.replacingOccurrences(of: key, with: value) } - + return newPath } } diff --git a/Sources/FormbricksSDK/Networking/ClientAPI/Endpoints/Environment/GetEnvironmentRequest.swift b/Sources/FormbricksSDK/Networking/ClientAPI/Endpoints/Environment/GetEnvironmentRequest.swift deleted file mode 100644 index 93e381ee..00000000 --- a/Sources/FormbricksSDK/Networking/ClientAPI/Endpoints/Environment/GetEnvironmentRequest.swift +++ /dev/null @@ -1,5 +0,0 @@ -struct GetEnvironmentRequest: CodableRequest { - typealias Response = EnvironmentResponse - var requestEndPoint: String { return FormbricksEnvironment.getEnvironmentRequestEndpoint } - var requestType: HTTPMethod { return .get } -} diff --git a/Sources/FormbricksSDK/Networking/ClientAPI/Endpoints/User/PostUserRequest.swift b/Sources/FormbricksSDK/Networking/ClientAPI/Endpoints/User/PostUserRequest.swift index 41078ed8..fd3f68cb 100644 --- a/Sources/FormbricksSDK/Networking/ClientAPI/Endpoints/User/PostUserRequest.swift +++ b/Sources/FormbricksSDK/Networking/ClientAPI/Endpoints/User/PostUserRequest.swift @@ -1,5 +1,5 @@ final class PostUserRequest: EncodableRequest, CodableRequest { - var requestEndPoint: String { return FormbricksEnvironment.postUserRequestEndpoint } + var requestEndPoint: String { return FormbricksWorkspace.postUserRequestEndpoint } var requestType: HTTPMethod { return .post } diff --git a/Sources/FormbricksSDK/Networking/ClientAPI/Endpoints/Workspace/GetWorkspaceRequest.swift b/Sources/FormbricksSDK/Networking/ClientAPI/Endpoints/Workspace/GetWorkspaceRequest.swift new file mode 100644 index 00000000..7da93bc1 --- /dev/null +++ b/Sources/FormbricksSDK/Networking/ClientAPI/Endpoints/Workspace/GetWorkspaceRequest.swift @@ -0,0 +1,5 @@ +struct GetWorkspaceRequest: CodableRequest { + typealias Response = WorkspaceResponse + var requestEndPoint: String { return FormbricksWorkspace.getWorkspaceStateRequestEndpoint } + var requestType: HTTPMethod { return .get } +} diff --git a/Sources/FormbricksSDK/Networking/Service/FormbricksService.swift b/Sources/FormbricksSDK/Networking/Service/FormbricksService.swift index 3d45f4c4..be29c023 100644 --- a/Sources/FormbricksSDK/Networking/Service/FormbricksService.swift +++ b/Sources/FormbricksSDK/Networking/Service/FormbricksService.swift @@ -1,10 +1,10 @@ /// FormbricksService is a service class that handles the network requests for Formbricks API. class FormbricksService: FormbricksServiceProtocol { - - // MARK: - Environment - - /// Get the current environment state. - func getEnvironmentState(completion: @escaping (ResultType) -> Void) { - let endPointRequest = GetEnvironmentRequest() + + // MARK: - Workspace - + /// Fetch the current workspace state. + func getWorkspaceState(completion: @escaping (ResultType) -> Void) { + let endPointRequest = GetWorkspaceRequest() execute(endPointRequest, withCompletion: completion) } @@ -17,8 +17,8 @@ class FormbricksService: FormbricksServiceProtocol { } protocol FormbricksServiceProtocol { - func getEnvironmentState( - completion: @escaping (ResultType) -> Void + func getWorkspaceState( + completion: @escaping (ResultType) -> Void ) func postUser( id: String, diff --git a/Sources/FormbricksSDK/WebView/FormbricksViewModel.swift b/Sources/FormbricksSDK/WebView/FormbricksViewModel.swift index 439c78b5..54e52a69 100644 --- a/Sources/FormbricksSDK/WebView/FormbricksViewModel.swift +++ b/Sources/FormbricksSDK/WebView/FormbricksViewModel.swift @@ -5,11 +5,11 @@ import SwiftUI final class FormbricksViewModel: ObservableObject { @Published var htmlString: String? let surveyId: String - - init(environmentResponse: EnvironmentResponse, surveyId: String) { + + init(workspaceResponse: WorkspaceResponse, surveyId: String) { self.surveyId = surveyId - if let webviewDataJson = WebViewData(environmentResponse: environmentResponse, surveyId: surveyId).getJsonString(), - let surveyScriptUrl = FormbricksEnvironment.surveyScriptUrlString { + if let webviewDataJson = WebViewData(workspaceResponse: workspaceResponse, surveyId: surveyId).getJsonString(), + let surveyScriptUrl = FormbricksWorkspace.surveyScriptUrlString { htmlString = htmlTemplate.replacingOccurrences(of: "{{WEBVIEW_DATA}}", with: webviewDataJson) .replacingOccurrences(of: "{{SURVEY_SCRIPT_URL}}", with: surveyScriptUrl) } @@ -23,7 +23,7 @@ private extension FormbricksViewModel { - + Formbricks WebView Survey @@ -43,11 +43,11 @@ private extension FormbricksViewModel { function onDisplayCreated() { window.webkit.messageHandlers.jsMessage.postMessage(JSON.stringify({ event: "onDisplayCreated" })); }; - + function onResponseCreated() { window.webkit.messageHandlers.jsMessage.postMessage(JSON.stringify({ event: "onResponseCreated" })); }; - + function onOpenExternalURL(url) { window.webkit.messageHandlers.jsMessage.postMessage(JSON.stringify({ event: "onOpenExternalURL", onOpenExternalURLParams: { url: url } })); }; @@ -55,7 +55,7 @@ private extension FormbricksViewModel { let setResponseFinished = null; function getSetIsResponseSendingFinished(callback) { setResponseFinished = callback; - } + } function loadSurvey() { const options = JSON.parse(json); @@ -83,47 +83,50 @@ private extension FormbricksViewModel { """ } - + } // MARK: - Helper class - private class WebViewData { var data: [String: Any] = [:] - - init(environmentResponse: EnvironmentResponse, surveyId: String) { - let matchedSurvey = environmentResponse.data.data.surveys?.first(where: {$0.id == surveyId}) - let project = environmentResponse.data.data.project - - data["survey"] = environmentResponse.getSurveyJson(forSurveyId: surveyId) + + init(workspaceResponse: WorkspaceResponse, surveyId: String) { + let matchedSurvey = workspaceResponse.data.data.surveys?.first(where: {$0.id == surveyId}) + let settings = workspaceResponse.data.data.settings + + data["survey"] = workspaceResponse.getSurveyJson(forSurveyId: surveyId) data["appUrl"] = Formbricks.appUrl - data["environmentId"] = Formbricks.environmentId + data["workspaceId"] = Formbricks.workspaceId + // Keep `environmentId` in the payload for backward compatibility with older + // survey-script versions that still read it. + data["environmentId"] = Formbricks.workspaceId data["contactId"] = Formbricks.userManager?.contactId data["isWebEnvironment"] = false - data["isBrandingEnabled"] = project.inAppSurveyBranding ?? true - + data["isBrandingEnabled"] = settings.inAppSurveyBranding ?? true + if let placementEnum = matchedSurvey?.projectOverwrites?.placement { data["placement"] = placementEnum.rawValue } else { - data["placement"] = project.placement + data["placement"] = settings.placement } - - data["clickOutside"] = matchedSurvey?.projectOverwrites?.clickOutsideClose ?? project.clickOutsideClose ?? false - data["overlay"] = (matchedSurvey?.projectOverwrites?.overlay ?? project.overlay ?? .none).rawValue - + + data["clickOutside"] = matchedSurvey?.projectOverwrites?.clickOutsideClose ?? settings.clickOutsideClose ?? false + data["overlay"] = (matchedSurvey?.projectOverwrites?.overlay ?? settings.overlay ?? .none).rawValue + let isMultiLangSurvey = (matchedSurvey?.languages?.count ?? 0) > 1 - + if isMultiLangSurvey { data["languageCode"] = Formbricks.language } else { data["languageCode"] = "default" } - + let hasCustomStyling = matchedSurvey?.styling != nil - let enabled = project.styling?.allowStyleOverwrite ?? false - - data["styling"] = hasCustomStyling && enabled ? environmentResponse.getSurveyStylingJson(forSurveyId: surveyId): environmentResponse.getProjectStylingJson() + let enabled = settings.styling?.allowStyleOverwrite ?? false + + data["styling"] = hasCustomStyling && enabled ? workspaceResponse.getSurveyStylingJson(forSurveyId: surveyId): workspaceResponse.getSettingsStylingJson() } - + func getJsonString() -> String? { do { let jsonData = try JSONSerialization.data(withJSONObject: data, options: []) @@ -133,5 +136,5 @@ private class WebViewData { return nil } } - + } diff --git a/Tests/FormbricksSDKTests/FormbricksSDKTests.swift b/Tests/FormbricksSDKTests/FormbricksSDKTests.swift index 65a3e8b9..c07d9f78 100644 --- a/Tests/FormbricksSDKTests/FormbricksSDKTests.swift +++ b/Tests/FormbricksSDKTests/FormbricksSDKTests.swift @@ -2,7 +2,7 @@ import XCTest @testable import FormbricksSDK final class FormbricksSDKTests: XCTestCase { - let environmentId = "environmentId" + let workspaceId = "workspaceId" let appUrl = "https://example.com" let userId = "6CCCE716-6783-4D0F-8344-9C7DFA43D8F7" let surveyID = "cm6ovw6j7000gsf0kduf4oo4i" @@ -13,12 +13,23 @@ final class FormbricksSDKTests: XCTestCase { super.setUp() // Always clean up before each test Formbricks.cleanup() + clearPersistedWorkspaceCache() } - + override func tearDown() { Formbricks.cleanup() + clearPersistedWorkspaceCache() super.tearDown() } + + /// Removes the cached workspace blobs from UserDefaults so tests don't leak + /// cache state into each other. `Formbricks.cleanup()` intentionally does + /// not wipe these keys because real apps rely on the cache across launches. + private func clearPersistedWorkspaceCache() { + let defaults = UserDefaults.standard + defaults.removeObject(forKey: SurveyManager.workspaceResponseObjectKey) + defaults.removeObject(forKey: SurveyManager.legacyEnvironmentResponseObjectKey) + } func testFormbricks() throws { // Everything should be in the default state before initialization. @@ -46,7 +57,7 @@ final class FormbricksSDKTests: XCTestCase { // Setup the SDK using your new instance-based design. // This creates new instances for both the UserManager and SurveyManager. - Formbricks.setup(with: FormbricksConfig.Builder(appUrl: appUrl, environmentId: environmentId) + Formbricks.setup(with: FormbricksConfig.Builder(appUrl: appUrl, workspaceId: workspaceId) .set(attributes: ["a": "b"]) .add(attribute: "test", forKey: "key") .setLogLevel(.debug) @@ -56,17 +67,17 @@ final class FormbricksSDKTests: XCTestCase { XCTAssertTrue(Formbricks.isInitialized) XCTAssertEqual(Formbricks.appUrl, appUrl) - XCTAssertEqual(Formbricks.environmentId, environmentId) + XCTAssertEqual(Formbricks.workspaceId, workspaceId) // Check error state handling. XCTAssertFalse(Formbricks.surveyManager?.hasApiError ?? false) mockService.isErrorResponseNeeded = true - Formbricks.surveyManager?.refreshEnvironmentIfNeeded(force: true) + Formbricks.surveyManager?.refreshWorkspaceIfNeeded(force: true) XCTAssertTrue(Formbricks.surveyManager?.hasApiError ?? false) mockService.isErrorResponseNeeded = false - Formbricks.surveyManager?.refreshEnvironmentIfNeeded(force: true) + Formbricks.surveyManager?.refreshWorkspaceIfNeeded(force: true) // Wait for environment to refresh let refreshExpectation = expectation(description: "Environment refreshed") @@ -91,7 +102,7 @@ final class FormbricksSDKTests: XCTestCase { XCTAssertNotNil(Formbricks.userManager?.syncTimer, "Sync timer should be set") // The environment should be fetched. - XCTAssertNotNil(Formbricks.surveyManager?.environmentResponse) + XCTAssertNotNil(Formbricks.surveyManager?.workspaceResponse) // Check if the filter method works properly. XCTAssertEqual(Formbricks.surveyManager?.filteredSurveys.count, 1) @@ -173,13 +184,13 @@ final class FormbricksSDKTests: XCTestCase { XCTAssertNil(Formbricks.presentSurveyManager) XCTAssertFalse(Formbricks.isInitialized) XCTAssertNil(Formbricks.appUrl) - XCTAssertNil(Formbricks.environmentId) + XCTAssertNil(Formbricks.workspaceId) XCTAssertNil(Formbricks.logger) } - + func testCleanupWithCompletion() { // Setup the SDK - let config = FormbricksConfig.Builder(appUrl: appUrl, environmentId: environmentId) + let config = FormbricksConfig.Builder(appUrl: appUrl, workspaceId: workspaceId) .setLogLevel(.debug) .service(mockService) .build() @@ -203,7 +214,7 @@ final class FormbricksSDKTests: XCTestCase { XCTAssertNil(Formbricks.apiQueue, "API queue should be nil") XCTAssertFalse(Formbricks.isInitialized, "SDK should not be initialized") XCTAssertNil(Formbricks.appUrl, "App URL should be nil") - XCTAssertNil(Formbricks.environmentId, "Environment ID should be nil") + XCTAssertNil(Formbricks.workspaceId, "Workspace ID should be nil") XCTAssertNil(Formbricks.logger, "Logger should be nil") } @@ -219,13 +230,15 @@ final class FormbricksSDKTests: XCTestCase { XCTAssertTrue(manager.shouldDisplayBasedOnPercentage(100)) XCTAssertFalse(manager.shouldDisplayBasedOnPercentage(0)) - // UserDefaults: corrupt data - UserDefaults.standard.set(Data([0x00, 0x01]), forKey: "environmentResponseObjectKey") - XCTAssertNil(manager.environmentResponse) + // UserDefaults: corrupt data under both the new and legacy keys so we exercise + // the fallback path too. + UserDefaults.standard.set(Data([0x00, 0x01]), forKey: SurveyManager.workspaceResponseObjectKey) + UserDefaults.standard.removeObject(forKey: SurveyManager.legacyEnvironmentResponseObjectKey) + XCTAssertNil(manager.workspaceResponse) - // Timer-based refresh: wait deterministically for the environment refresh notification - let notificationExpectation = expectation(forNotification: .environmentRefreshed, object: manager, handler: nil) - manager.refreshEnvironmentAfter(timeout: 0.1) + // Timer-based refresh: wait deterministically for the workspace refresh notification + let notificationExpectation = expectation(forNotification: .workspaceRefreshed, object: manager, handler: nil) + manager.refreshWorkspaceAfter(timeout: 0.1) wait(for: [notificationExpectation], timeout: 2.0) // getLanguageCode coverage @@ -267,14 +280,14 @@ final class FormbricksSDKTests: XCTestCase { let errorsMockService = MockFormbricksService() errorsMockService.userMockResponse = .userWithErrors - let config = FormbricksConfig.Builder(appUrl: appUrl, environmentId: environmentId) + let config = FormbricksConfig.Builder(appUrl: appUrl, workspaceId: workspaceId) .setLogLevel(.debug) .service(errorsMockService) .build() Formbricks.setup(with: config) // Refresh environment first - Formbricks.surveyManager?.refreshEnvironmentIfNeeded(force: true) + Formbricks.surveyManager?.refreshWorkspaceIfNeeded(force: true) let envExpectation = expectation(description: "Env loaded") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { envExpectation.fulfill() } wait(for: [envExpectation]) @@ -297,14 +310,14 @@ final class FormbricksSDKTests: XCTestCase { let messagesMockService = MockFormbricksService() messagesMockService.userMockResponse = .userWithMessages - let config = FormbricksConfig.Builder(appUrl: appUrl, environmentId: environmentId) + let config = FormbricksConfig.Builder(appUrl: appUrl, workspaceId: workspaceId) .setLogLevel(.debug) .service(messagesMockService) .build() Formbricks.setup(with: config) // Refresh environment first - Formbricks.surveyManager?.refreshEnvironmentIfNeeded(force: true) + Formbricks.surveyManager?.refreshWorkspaceIfNeeded(force: true) let envExpectation = expectation(description: "Env loaded") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { envExpectation.fulfill() } wait(for: [envExpectation]) @@ -327,14 +340,14 @@ final class FormbricksSDKTests: XCTestCase { let bothMockService = MockFormbricksService() bothMockService.userMockResponse = .userWithErrorsAndMessages - let config = FormbricksConfig.Builder(appUrl: appUrl, environmentId: environmentId) + let config = FormbricksConfig.Builder(appUrl: appUrl, workspaceId: workspaceId) .setLogLevel(.debug) .service(bothMockService) .build() Formbricks.setup(with: config) // Refresh environment first - Formbricks.surveyManager?.refreshEnvironmentIfNeeded(force: true) + Formbricks.surveyManager?.refreshWorkspaceIfNeeded(force: true) let envExpectation = expectation(description: "Env loaded") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { envExpectation.fulfill() } wait(for: [envExpectation]) @@ -356,14 +369,14 @@ final class FormbricksSDKTests: XCTestCase { // MARK: - setUserId override behavior tests func testSetUserIdSameValueIsNoOp() { - let config = FormbricksConfig.Builder(appUrl: appUrl, environmentId: environmentId) + let config = FormbricksConfig.Builder(appUrl: appUrl, workspaceId: workspaceId) .setLogLevel(.debug) .service(mockService) .build() Formbricks.setup(with: config) // Refresh environment first - Formbricks.surveyManager?.refreshEnvironmentIfNeeded(force: true) + Formbricks.surveyManager?.refreshWorkspaceIfNeeded(force: true) let envExpectation = expectation(description: "Env loaded") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { envExpectation.fulfill() } wait(for: [envExpectation]) @@ -382,14 +395,14 @@ final class FormbricksSDKTests: XCTestCase { } func testSetUserIdDifferentValueOverridesPrevious() { - let config = FormbricksConfig.Builder(appUrl: appUrl, environmentId: environmentId) + let config = FormbricksConfig.Builder(appUrl: appUrl, workspaceId: workspaceId) .setLogLevel(.debug) .service(mockService) .build() Formbricks.setup(with: config) // Refresh environment first - Formbricks.surveyManager?.refreshEnvironmentIfNeeded(force: true) + Formbricks.surveyManager?.refreshWorkspaceIfNeeded(force: true) let envExpectation = expectation(description: "Env loaded") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { envExpectation.fulfill() } wait(for: [envExpectation]) @@ -419,7 +432,7 @@ final class FormbricksSDKTests: XCTestCase { } func testLogoutWithoutUserIdDoesNotError() { - let config = FormbricksConfig.Builder(appUrl: appUrl, environmentId: environmentId) + let config = FormbricksConfig.Builder(appUrl: appUrl, workspaceId: workspaceId) .setLogLevel(.debug) .service(mockService) .build() @@ -434,7 +447,7 @@ final class FormbricksSDKTests: XCTestCase { // MARK: - setAttribute overload tests func testSetAttributeDouble() { - let config = FormbricksConfig.Builder(appUrl: appUrl, environmentId: environmentId) + let config = FormbricksConfig.Builder(appUrl: appUrl, workspaceId: workspaceId) .setLogLevel(.debug) .service(mockService) .build() @@ -445,7 +458,7 @@ final class FormbricksSDKTests: XCTestCase { } func testSetAttributeDate() { - let config = FormbricksConfig.Builder(appUrl: appUrl, environmentId: environmentId) + let config = FormbricksConfig.Builder(appUrl: appUrl, workspaceId: workspaceId) .setLogLevel(.debug) .service(mockService) .build() @@ -458,7 +471,7 @@ final class FormbricksSDKTests: XCTestCase { // MARK: - ConfigBuilder coverage tests func testConfigBuilderStringAttributes() { - let config = FormbricksConfig.Builder(appUrl: appUrl, environmentId: environmentId) + let config = FormbricksConfig.Builder(appUrl: appUrl, workspaceId: workspaceId) .set(stringAttributes: ["key1": "val1", "key2": "val2"]) .build() @@ -467,7 +480,7 @@ final class FormbricksSDKTests: XCTestCase { } func testConfigBuilderAddAttribute() { - let config = FormbricksConfig.Builder(appUrl: appUrl, environmentId: environmentId) + let config = FormbricksConfig.Builder(appUrl: appUrl, workspaceId: workspaceId) .add(attribute: "hello", forKey: "greeting") .build() @@ -479,25 +492,25 @@ final class FormbricksSDKTests: XCTestCase { func testPresentCompletesInHeadlessEnvironment() { // In a headless test environment there is no key window, so present() should // call the completion with false. - let config = FormbricksConfig.Builder(appUrl: appUrl, environmentId: environmentId) + let config = FormbricksConfig.Builder(appUrl: appUrl, workspaceId: workspaceId) .setLogLevel(.debug) .service(mockService) .build() Formbricks.setup(with: config) - Formbricks.surveyManager?.refreshEnvironmentIfNeeded(force: true) + Formbricks.surveyManager?.refreshWorkspaceIfNeeded(force: true) let loadExpectation = expectation(description: "Env loaded") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { loadExpectation.fulfill() } wait(for: [loadExpectation]) - guard let env = Formbricks.surveyManager?.environmentResponse else { - XCTFail("Missing environmentResponse") + guard let workspace = Formbricks.surveyManager?.workspaceResponse else { + XCTFail("Missing workspaceResponse") return } let manager = PresentSurveyManager() let presentExpectation = expectation(description: "Present completes") - manager.present(environmentResponse: env, id: surveyID) { success in + manager.present(workspaceResponse: workspace, id: surveyID) { success in // No key window in headless tests → completion(false) XCTAssertFalse(success, "Presentation should fail in headless environment") presentExpectation.fulfill() @@ -509,25 +522,25 @@ final class FormbricksSDKTests: XCTestCase { func testWebViewDataUsesSurveyOverwrites() { // Setup SDK with mock service loading Environment.json (which now includes projectOverwrites) - let config = FormbricksConfig.Builder(appUrl: appUrl, environmentId: environmentId) + let config = FormbricksConfig.Builder(appUrl: appUrl, workspaceId: workspaceId) .setLogLevel(.debug) .service(mockService) .build() Formbricks.setup(with: config) // Force refresh and wait briefly for async fetch - Formbricks.surveyManager?.refreshEnvironmentIfNeeded(force: true) + Formbricks.surveyManager?.refreshWorkspaceIfNeeded(force: true) let expectation = self.expectation(description: "Env loaded") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { expectation.fulfill() } wait(for: [expectation]) - guard let env = Formbricks.surveyManager?.environmentResponse else { - XCTFail("Missing environmentResponse") + guard let workspace = Formbricks.surveyManager?.workspaceResponse else { + XCTFail("Missing workspaceResponse") return } // Build the view model to produce WEBVIEW_DATA - let vm = FormbricksViewModel(environmentResponse: env, surveyId: surveyID) + let vm = FormbricksViewModel(workspaceResponse: workspace, surveyId: surveyID) guard let html = vm.htmlString else { XCTFail("Missing htmlString") return @@ -556,5 +569,187 @@ final class FormbricksSDKTests: XCTestCase { XCTAssertEqual(object["placement"] as? String, "center") XCTAssertEqual(object["overlay"] as? String, "dark") XCTAssertEqual(object["clickOutside"] as? Bool, false) + + // WEBVIEW_DATA should include workspaceId (plus environmentId alias for back-compat) + XCTAssertEqual(object["workspaceId"] as? String, workspaceId) + XCTAssertEqual(object["environmentId"] as? String, workspaceId) + } + + // MARK: - workspaceId / environmentId parameter tests + + /// The deprecated `environmentId` init is still supported for backward compatibility. + @available(*, deprecated) + func testSetupWithDeprecatedEnvironmentId() { + let legacyId = "legacy-env-id" + let config = FormbricksConfig.Builder(appUrl: appUrl, environmentId: legacyId) + .setLogLevel(.debug) + .service(mockService) + .build() + Formbricks.setup(with: config) + + XCTAssertTrue(Formbricks.isInitialized) + XCTAssertEqual(Formbricks.workspaceId, legacyId, "environmentId should be stored as workspaceId") + XCTAssertTrue(config.usedDeprecatedEnvironmentId) + } + + /// New `workspaceId` init does not mark the config as using a deprecated parameter. + func testSetupWithWorkspaceIdDoesNotFlagDeprecation() { + let config = FormbricksConfig.Builder(appUrl: appUrl, workspaceId: workspaceId) + .setLogLevel(.debug) + .service(mockService) + .build() + Formbricks.setup(with: config) + + XCTAssertTrue(Formbricks.isInitialized) + XCTAssertEqual(Formbricks.workspaceId, workspaceId) + XCTAssertFalse(config.usedDeprecatedEnvironmentId) + } + + /// The legacy `Formbricks.environmentId` accessor still returns the canonical id. + @available(*, deprecated) + func testLegacyEnvironmentIdAccessorMirrorsWorkspaceId() { + let config = FormbricksConfig.Builder(appUrl: appUrl, workspaceId: workspaceId) + .service(mockService) + .build() + Formbricks.setup(with: config) + + XCTAssertEqual(Formbricks.environmentId, workspaceId) + XCTAssertEqual(Formbricks.environmentId, Formbricks.workspaceId) + } + + // MARK: - Tolerant decoding tests + + /// Workspace data should decode when the server sends the new `settings` key. + func testWorkspaceDataDecodesFromSettingsKey() throws { + let json = """ + { + "data": { + "data": { + "settings": { + "recontactDays": 7, + "clickOutsideClose": true, + "overlay": "none", + "placement": "bottomRight", + "inAppSurveyBranding": true, + "styling": { "allowStyleOverwrite": true } + }, + "surveys": [], + "actionClasses": [] + }, + "expiresAt": "2099-12-31T23:59:59.999Z" + } + } + """.data(using: .utf8)! + + let response = try JSONDecoder.iso8601Full.decode(WorkspaceResponse.self, from: json) + XCTAssertEqual(response.data.data.settings.recontactDays, 7) + XCTAssertEqual(response.data.data.settings.placement, "bottomRight") + } + + /// Workspace data should decode when the server sends the `workspace` key. + func testWorkspaceDataDecodesFromWorkspaceKey() throws { + let json = """ + { + "data": { + "data": { + "workspace": { + "recontactDays": 3, + "clickOutsideClose": false, + "overlay": "none", + "placement": "center", + "inAppSurveyBranding": false, + "styling": { "allowStyleOverwrite": false } + }, + "surveys": [], + "actionClasses": [] + }, + "expiresAt": "2099-12-31T23:59:59.999Z" + } + } + """.data(using: .utf8)! + + let response = try JSONDecoder.iso8601Full.decode(WorkspaceResponse.self, from: json) + XCTAssertEqual(response.data.data.settings.recontactDays, 3) + XCTAssertEqual(response.data.data.settings.placement, "center") + } + + /// Workspace data should still decode when the server sends the legacy `project` key, + /// which lets the SDK read cached blobs written by older SDK versions. + func testWorkspaceDataDecodesFromLegacyProjectKey() throws { + let json = """ + { + "data": { + "data": { + "project": { + "recontactDays": 14, + "clickOutsideClose": true, + "overlay": "none", + "placement": "bottomLeft", + "inAppSurveyBranding": true, + "styling": { "allowStyleOverwrite": true } + }, + "surveys": [], + "actionClasses": [] + }, + "expiresAt": "2099-12-31T23:59:59.999Z" + } + } + """.data(using: .utf8)! + + let response = try JSONDecoder.iso8601Full.decode(WorkspaceResponse.self, from: json) + XCTAssertEqual(response.data.data.settings.recontactDays, 14) + XCTAssertEqual(response.data.data.settings.placement, "bottomLeft") + } + + // MARK: - UserDefaults migration tests + + /// A cache blob written under the pre-rename key should be read once, migrated to + /// the new key, and then removed from the legacy slot. + func testLegacyEnvironmentResponseCacheIsMigratedOnRead() throws { + // Clear both keys to start from a known state. + let defaults = UserDefaults.standard + defaults.removeObject(forKey: SurveyManager.workspaceResponseObjectKey) + defaults.removeObject(forKey: SurveyManager.legacyEnvironmentResponseObjectKey) + + // Build a blob the way the old SDK version would have written it: decode the + // fixture with the SDK's iso8601 decoder, then re-encode with the plain + // JSONEncoder used by the persisted-cache path. + guard let fixtureUrl = Bundle.module.url(forResource: "Environment", withExtension: "json"), + let fixtureData = try? Data(contentsOf: fixtureUrl), + let fixtureResponse = try? JSONDecoder.iso8601Full.decode(WorkspaceResponse.self, from: fixtureData), + let legacyBlob = try? JSONEncoder().encode(fixtureResponse) else { + XCTFail("Missing or invalid Environment.json fixture") + return + } + defaults.set(legacyBlob, forKey: SurveyManager.legacyEnvironmentResponseObjectKey) + + // Fresh SurveyManager so the in-memory backing cache is empty. + let userManager = UserManager() + let presentSurveyManager = PresentSurveyManager() + let manager = SurveyManager.create(userManager: userManager, presentSurveyManager: presentSurveyManager, service: MockFormbricksService()) + + // Reading should migrate and return a decoded WorkspaceResponse. + XCTAssertNotNil(manager.workspaceResponse, "Legacy cache should be read and decoded") + + // Legacy key is gone, new key is populated. + XCTAssertNil(defaults.data(forKey: SurveyManager.legacyEnvironmentResponseObjectKey)) + XCTAssertNotNil(defaults.data(forKey: SurveyManager.workspaceResponseObjectKey)) + } + + // MARK: - Notification back-compat + + /// Subscribers of the deprecated `.environmentRefreshed` should still get notified + /// while we also post the new `.workspaceRefreshed` name. + func testEnvironmentRefreshedNotificationStillFiresForBackwardCompat() { + let userManager = UserManager() + let presentSurveyManager = PresentSurveyManager() + let manager = SurveyManager.create(userManager: userManager, presentSurveyManager: presentSurveyManager, service: MockFormbricksService()) + + let legacyExpectation = expectation(forNotification: Notification.Name("Formbricks.environmentRefreshed"), object: manager, handler: nil) + let newExpectation = expectation(forNotification: .workspaceRefreshed, object: manager, handler: nil) + + manager.refreshWorkspaceAfter(timeout: 0.1) + + wait(for: [legacyExpectation, newExpectation], timeout: 2.0) } } diff --git a/Tests/FormbricksSDKTests/FormbricksEnvironmentTests.swift b/Tests/FormbricksSDKTests/FormbricksWorkspaceTests.swift similarity index 63% rename from Tests/FormbricksSDKTests/FormbricksEnvironmentTests.swift rename to Tests/FormbricksSDKTests/FormbricksWorkspaceTests.swift index 1f6d521a..27496dba 100644 --- a/Tests/FormbricksSDKTests/FormbricksEnvironmentTests.swift +++ b/Tests/FormbricksSDKTests/FormbricksWorkspaceTests.swift @@ -1,51 +1,51 @@ import XCTest @testable import FormbricksSDK -final class FormbricksEnvironmentTests: XCTestCase { - +final class FormbricksWorkspaceTests: XCTestCase { + override func setUp() { super.setUp() // Always clean up before each test Formbricks.cleanup() } - + override func tearDown() { Formbricks.cleanup() super.tearDown() } - + func testBaseApiUrl() { // Test that baseApiUrl returns nil when appUrl is nil - XCTAssertNil(FormbricksEnvironment.baseApiUrl) - + XCTAssertNil(FormbricksWorkspace.baseApiUrl) + // Setup SDK with valid appUrl - Formbricks.setup(with: FormbricksConfig.Builder(appUrl: "https://app.formbricks.com", environmentId: "test-env-id") + Formbricks.setup(with: FormbricksConfig.Builder(appUrl: "https://app.formbricks.com", workspaceId: "test-workspace-id") .setLogLevel(.debug) .build()) - + // Test that baseApiUrl returns the correct URL - XCTAssertEqual(FormbricksEnvironment.baseApiUrl, "https://app.formbricks.com") + XCTAssertEqual(FormbricksWorkspace.baseApiUrl, "https://app.formbricks.com") } - + func testSurveyScriptUrlString() { // Test that surveyScriptUrlString returns nil when appUrl is nil - XCTAssertNil(FormbricksEnvironment.surveyScriptUrlString) - + XCTAssertNil(FormbricksWorkspace.surveyScriptUrlString) + // Setup SDK with valid appUrl - Formbricks.setup(with: FormbricksConfig.Builder(appUrl: "https://app.formbricks.com", environmentId: "test-env-id") + Formbricks.setup(with: FormbricksConfig.Builder(appUrl: "https://app.formbricks.com", workspaceId: "test-workspace-id") .setLogLevel(.debug) .build()) - + // Test that surveyScriptUrlString returns the correct URL - XCTAssertEqual(FormbricksEnvironment.surveyScriptUrlString, "https://app.formbricks.com/js/surveys.umd.cjs") - + XCTAssertEqual(FormbricksWorkspace.surveyScriptUrlString, "https://app.formbricks.com/js/surveys.umd.cjs") + // Test with invalid URL Formbricks.cleanup() - Formbricks.setup(with: FormbricksConfig.Builder(appUrl: "invalid url", environmentId: "test-env-id") + Formbricks.setup(with: FormbricksConfig.Builder(appUrl: "invalid url", workspaceId: "test-workspace-id") .setLogLevel(.debug) .build()) - + // Test that surveyScriptUrlString returns nil for invalid URL - XCTAssertNil(FormbricksEnvironment.surveyScriptUrlString) + XCTAssertNil(FormbricksWorkspace.surveyScriptUrlString) } -} \ No newline at end of file +} diff --git a/Tests/FormbricksSDKTests/MockFormbricksService/MockFormbricksService.swift b/Tests/FormbricksSDKTests/MockFormbricksService/MockFormbricksService.swift index 0a560dda..d86c5e1a 100644 --- a/Tests/FormbricksSDKTests/MockFormbricksService/MockFormbricksService.swift +++ b/Tests/FormbricksSDKTests/MockFormbricksService/MockFormbricksService.swift @@ -17,7 +17,7 @@ class MockFormbricksService: FormbricksService { /// Defaults to `.user` (the standard User.json). var userMockResponse: MockResponse = .user - override func getEnvironmentState(completion: @escaping (ResultType) -> Void) { + override func getWorkspaceState(completion: @escaping (ResultType) -> Void) { if isErrorResponseNeeded { completion(.failure(RuntimeError(message: ""))) } else { diff --git a/Tests/FormbricksSDKTests/Networking/APIClientTests.swift b/Tests/FormbricksSDKTests/Networking/APIClientTests.swift index e8b61bc9..abb8c495 100644 --- a/Tests/FormbricksSDKTests/Networking/APIClientTests.swift +++ b/Tests/FormbricksSDKTests/Networking/APIClientTests.swift @@ -56,13 +56,13 @@ final class APIClientTests: XCTestCase { override func setUp() { super.setUp() mockURLSession = MockURLSession() - Formbricks.environmentId = "test-env-id" + Formbricks.workspaceId = "test-workspace-id" } - + override func tearDown() { mockURLSession = nil sut = nil - Formbricks.environmentId = nil + Formbricks.workspaceId = nil super.tearDown() } @@ -627,11 +627,11 @@ final class APIClientTests: XCTestCase { wait(for: [expectation], timeout: 1.0) } - func testEnvironmentResponse() { + func testWorkspaceResponse() { // Given let expectation = XCTestExpectation(description: "API call completes") - - let request = GetEnvironmentRequest() + + let request = GetWorkspaceRequest() Formbricks.appUrl = "https://api.test.com" addTeardownBlock { Formbricks.appUrl = nil diff --git a/Tests/FormbricksSDKTests/Networking/ClientAPIEndpointsTests.swift b/Tests/FormbricksSDKTests/Networking/ClientAPIEndpointsTests.swift index d91729a2..2c93191a 100644 --- a/Tests/FormbricksSDKTests/Networking/ClientAPIEndpointsTests.swift +++ b/Tests/FormbricksSDKTests/Networking/ClientAPIEndpointsTests.swift @@ -1,9 +1,9 @@ import XCTest @testable import FormbricksSDK -final class GetEnvironmentRequestTests: XCTestCase { +final class GetWorkspaceRequestTests: XCTestCase { func testInit() { - let req = GetEnvironmentRequest() + let req = GetWorkspaceRequest() XCTAssertEqual(req.requestType, .get) XCTAssertFalse(req.requestEndPoint.isEmpty) } @@ -15,4 +15,4 @@ final class PostUserRequestTests: XCTestCase { XCTAssertEqual(req.requestType, .post) XCTAssertFalse(req.requestEndPoint.isEmpty) } -} \ No newline at end of file +}