Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Sources/FormbricksSDK/Extension/Calendar+DaysBetween.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ extension Calendar {
let toDate = startOfDay(for: to)
let numberOfDays = dateComponents([.day], from: fromDate, to: toDate)

return numberOfDays.day! + 1
guard let day = numberOfDays.day else { return 0 }
return abs(day + 1)
}
}
94 changes: 57 additions & 37 deletions Sources/FormbricksSDK/Formbricks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ public protocol FormbricksDelegate: AnyObject {

/// The main class of the Formbricks SDK. It contains the main methods to interact with the SDK.
@objc(Formbricks) public class Formbricks: NSObject {

static internal var appUrl: String?
static internal var environmentId: String?
static internal var language: String = "default"
static internal var isInitialized: Bool = false

static internal var userManager: UserManager?
static internal var presentSurveyManager: PresentSurveyManager?
static internal var surveyManager: SurveyManager?
Expand All @@ -40,24 +40,24 @@ public protocol FormbricksDelegate: AnyObject {

// make this class not instantiatable outside of the SDK
internal override init() {
/*
/*
This empty initializer prevents external instantiation of the Formbricks class.
All methods are static and the class serves as a namespace for the SDK,
so instance creation is not needed and should be restricted.
*/
}

/**
Initializes the Formbricks SDK with the given config ``FormbricksConfig``.
This method is mandatory to be called, and should be only once per application lifecycle.

Example:
```swift
let config = FormbricksConfig.Builder(appUrl: "APP_URL_HERE", environmentId: "TOKEN_HERE")
.setUserId("USER_ID_HERE")
.setLogLevel(.debug)
.build()

Formbricks.setup(with: config)
```
*/
Expand All @@ -66,24 +66,39 @@ public protocol FormbricksDelegate: AnyObject {
certData: Data? = nil) {
logger = Logger()
apiQueue = OperationQueue()

if force {
isInitialized = false
}

guard !isInitialized else {
let error = FormbricksSDKError(type: .sdkIsAlreadyInitialized)
delegate?.onError(error)
Formbricks.logger?.error(error.message)
return
}

self.appUrl = config.appUrl
self.environmentId = config.environmentId
self.logger?.logLevel = config.logLevel
// Validate appUrl before proceeding with setup
guard let url = URL(string: config.appUrl) else {
let error = FormbricksSDKError(type: .invalidAppUrl)
Formbricks.logger?.error("Invalid appUrl: \(config.appUrl). SDK setup aborted.")
return
}

// 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
}

let svc: FormbricksServiceProtocol = config.customService ?? FormbricksService()
self.securityCertData = certData

userManager = UserManager()
userManager?.service = svc
if let userId = config.userId {
userManager?.set(userId: userId)
}
Expand All @@ -96,20 +111,20 @@ public protocol FormbricksDelegate: AnyObject {
}

presentSurveyManager = PresentSurveyManager()
surveyManager = SurveyManager.create(userManager: userManager!, presentSurveyManager: presentSurveyManager!)
surveyManager = SurveyManager.create(userManager: userManager!, presentSurveyManager: presentSurveyManager!, service: svc)
userManager?.surveyManager = surveyManager

surveyManager?.refreshEnvironmentIfNeeded(force: force,
isInitial: true)
userManager?.syncUserStateIfNeeded()

self.isInitialized = true
}

/**
Sets the user id for the current user with the given `String`.
The SDK must be initialized before calling this method.

Example:
```swift
Formbricks.setUserId("USER_ID_HERE")
Expand All @@ -122,19 +137,19 @@ public protocol FormbricksDelegate: AnyObject {
Formbricks.logger?.error(error.message)
return
}

if let existing = userManager?.userId, !existing.isEmpty {
logger?.error("A userId is already set (\"\(existing)\") – please call Formbricks.logout() before setting a new one.")
return
}

userManager?.set(userId: userId)
}

/**
Adds an attribute for the current user with the given `String` value and `String` key.
The SDK must be initialized before calling this method.

Example:
```swift
Formbricks.setAttribute("ATTRIBUTE", forKey: "KEY")
Expand All @@ -147,14 +162,14 @@ public protocol FormbricksDelegate: AnyObject {
Formbricks.logger?.error(error.message)
return
}

userManager?.add(attribute: attribute, forKey: key)
}

/**
Sets the user attributes for the current user with the given `Dictionary` of `String` values and `String` keys.
The SDK must be initialized before calling this method.

Example:
```swift
Formbricks.setAttributes(["KEY", "ATTRIBUTE"])
Expand All @@ -167,20 +182,21 @@ public protocol FormbricksDelegate: AnyObject {
Formbricks.logger?.error(error.message)
return
}

userManager?.set(attributes: attributes)
}

/**
Sets the language for the current user with the given `String`.
The SDK must be initialized before calling this method.

This method can be called before or after SDK initialization.
Example:
```swift
Formbricks.setLanguage("de")
```
*/
@objc public static func setLanguage(_ language: String) {
// Set the language property regardless of initialization status
guard Formbricks.isInitialized else {
let error = FormbricksSDKError(type: .sdkIsNotInitialized)
delegate?.onError(error)
Expand All @@ -191,42 +207,46 @@ public protocol FormbricksDelegate: AnyObject {
if (Formbricks.language == language) {
return
}

Formbricks.language = language
userManager?.set(language: language)

// Only update the user manager if SDK is initialized
if Formbricks.isInitialized {
userManager?.set(language: language)
}
}

/**
Tracks an action with the given `String`. The SDK will process the action and it will present the survey if any of them can be triggered.
The SDK must be initialized before calling this method.

Example:
```swift
Formbricks.track("button_clicked")
```
*/
@objc public static func track(_ action: String, hiddenFields: [String: Any]? = nil) {
@objc public static func track(_ action: String, completion: (() -> Void)? = nil, hiddenFields: [String: Any]? = nil) {
guard Formbricks.isInitialized else {
let error = FormbricksSDKError(type: .sdkIsNotInitialized)
delegate?.onError(error)
Formbricks.logger?.error(error.message)
return
}

Formbricks.isInternetAvailabile { available in
if available {
surveyManager?.track(action, hiddenFields: hiddenFields)
surveyManager?.track(action, completion: completion, hiddenFields: hiddenFields)
} else {
Formbricks.logger?.warning(FormbricksSDKError.init(type: .networkError).message)
}
}

}

/**
Logs out the current user. This will clear the user attributes and the user id.
The SDK must be initialized before calling this method.

Example:
```swift
Formbricks.logout()
Expand All @@ -243,7 +263,7 @@ public protocol FormbricksDelegate: AnyObject {
userManager?.logout()
Formbricks.delegate?.onSuccess(.onFinishedLogout)
}

/**
Cleans up the SDK. This will clear the user attributes, the user id and the environment state.
The SDK must be initialized before calling this method.
Expand All @@ -260,7 +280,7 @@ public protocol FormbricksDelegate: AnyObject {
}
```
*/

@objc public static func cleanup(waitForOperations: Bool = false, completion: (() -> Void)? = nil) {
if waitForOperations, let queue = apiQueue {
DispatchQueue.global(qos: .background).async {
Expand Down
24 changes: 17 additions & 7 deletions Sources/FormbricksSDK/Helpers/ConfigBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import Foundation
let userId: String?
let attributes: [String:String]?
let logLevel: LogLevel
/// Optional custom service, injected via Builder
let customService: FormbricksServiceProtocol?

init(appUrl: String, environmentId: String, userId: String?, attributes: [String : String]?, logLevel: LogLevel) {
self.appUrl = appUrl
self.environmentId = environmentId
self.userId = userId
self.attributes = attributes
self.logLevel = logLevel
init(appUrl: String, environmentId: String, userId: String?, attributes: [String : String]?, logLevel: LogLevel, customService: FormbricksServiceProtocol?) {
self.appUrl = appUrl
self.environmentId = environmentId
self.userId = userId
self.attributes = attributes
self.logLevel = logLevel
self.customService = customService
}

/// The builder class for the FormbricksConfig object.
Expand All @@ -23,6 +26,8 @@ import Foundation
var userId: String?
var attributes: [String:String] = [:]
var logLevel: LogLevel = .error
/// Optional custom service, injected via Builder
var customService: FormbricksServiceProtocol?

@objc public init(appUrl: String, environmentId: String) {
self.appUrl = appUrl
Expand Down Expand Up @@ -53,9 +58,14 @@ import Foundation
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)
return FormbricksConfig(appUrl: appUrl, environmentId: environmentId, userId: userId, attributes: attributes, logLevel: logLevel, customService: customService)
}
}
}
32 changes: 15 additions & 17 deletions Sources/FormbricksSDK/Helpers/FormbricksEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,29 @@ import Foundation

internal enum FormbricksEnvironment {

/// Only `appUrl` is user-supplied. Crash early if it’s missing.
fileprivate static var baseApiUrl: String {
guard let url = Formbricks.appUrl else {
fatalError("Formbricks.setup must be called before using the SDK.")
}
return url
/// 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 {
let path = "/" + ["js", "surveys.umd.cjs"].joined(separator: "/")
return baseApiUrl + path
static var surveyScriptUrlString: String? {
guard let baseURLString = baseApiUrl,
let baseURL = URL(string: baseURLString),
baseURL.scheme == "https" 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
static var getEnvironmentRequestEndpoint: String {
return ["api", "v2", "client", "{environmentId}", "environment"].joined(separator: "/")
}

/// Returns the full post-user URL as a String for the given ID
static var postUserRequestEndpoint: String {
let path = "/" + ["api", "v2", "client", "{environmentId}", "user"]
.joined(separator: "/")
return path
static var postUserRequestEndpoint: String {
return ["api", "v2", "client", "{environmentId}", "user"].joined(separator: "/")
}
}
10 changes: 5 additions & 5 deletions Sources/FormbricksSDK/Manager/PresentSurveyManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ final class PresentSurveyManager {
The class serves as a namespace for the present method, so instance creation is not needed and should be restricted.
*/
}

/// The view controller that will present the survey window.
private weak var viewController: UIViewController?

/// Present the webview
func present(environmentResponse: EnvironmentResponse, id: String, hiddenFields: [String: Any]? = nil) {
DispatchQueue.main.async { [weak self] in
Expand All @@ -20,16 +20,16 @@ final class PresentSurveyManager {
let view = FormbricksView(viewModel: FormbricksViewModel(environmentResponse: environmentResponse, surveyId: id, hiddenFields: hiddenFields))
let vc = UIHostingController(rootView: view)
vc.modalPresentationStyle = .overCurrentContext
vc.view.backgroundColor = UIColor.gray.withAlphaComponent(0.6)
vc.view.backgroundColor = UIColor.clear//UIColor.gray.withAlphaComponent(0.6)
if let presentationController = vc.presentationController as? UISheetPresentationController {
presentationController.detents = [.large()]
}
self.viewController = vc
topVC.present(vc, animated: true, completion: nil)
topVC.present(vc, animated: false, completion: nil)
}
}
}

/// Dismiss the webview
func dismissView() {
viewController?.dismiss(animated: false)
Expand Down
Loading