diff --git a/.gitignore b/.gitignore index aa874edc93..8c8df66958 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,4 @@ infra/terraform/.terraform/ # IntelliJ IDEA /.idea/ /*.iml +packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/project.xcworkspace/xcuserdata diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 1840f070d1..d5e1abf85e 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -127,7 +127,7 @@ const nextConfig = { }, { // matching all API routes - source: "/api/v1/client/:path*", + source: "/api/(v1|v2)/client/:path*", headers: [ { key: "Access-Control-Allow-Credentials", value: "true" }, { key: "Access-Control-Allow-Origin", value: "*" }, diff --git a/packages/android/app/src/main/java/com/formbricks/demo/MainActivity.kt b/packages/android/app/src/main/java/com/formbricks/demo/MainActivity.kt index 0ef212b663..5679e0f700 100644 --- a/packages/android/app/src/main/java/com/formbricks/demo/MainActivity.kt +++ b/packages/android/app/src/main/java/com/formbricks/demo/MainActivity.kt @@ -1,12 +1,14 @@ package com.formbricks.demo import android.os.Bundle +import android.util.Log import android.widget.Button import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import com.formbricks.formbrickssdk.Formbricks +import com.formbricks.formbrickssdk.FormbricksCallback import com.formbricks.formbrickssdk.helper.FormbricksConfig import java.util.UUID @@ -15,6 +17,25 @@ class MainActivity : AppCompatActivity() { super.onCreate(savedInstanceState) enableEdgeToEdge() + Formbricks.callback = object: FormbricksCallback { + override fun onSurveyStarted() { + Log.d("FormbricksCallback", "onSurveyStarted") + } + + override fun onSurveyFinished() { + Log.d("FormbricksCallback", "onSurveyFinished") + } + + override fun onSurveyClosed() { + Log.d("FormbricksCallback", "onSurveyClosed") + } + + override fun onError(error: Exception) { + Log.d("FormbricksCallback", "onError: ${error.localizedMessage}") + } + + } + val config = FormbricksConfig.Builder("[appUrl]","[environmentId]") .setLoggingEnabled(true) .setFragmentManager(supportFragmentManager) @@ -35,4 +56,4 @@ class MainActivity : AppCompatActivity() { Formbricks.track("click_demo_button") } } -} \ No newline at end of file +} diff --git a/packages/ios/Formbricks.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/ios/Formbricks.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000000..e341bff87d --- /dev/null +++ b/packages/ios/Formbricks.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "92c0230fb0adc404299bb05aba6c51a76f86c388fdfb9f4e9bed3a757f80fc07", + "pins" : [ + { + "identity" : "anycodable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Flight-School/AnyCodable", + "state" : { + "revision" : "862808b2070cd908cb04f9aafe7de83d35f81b05", + "version" : "0.6.7" + } + } + ], + "version" : 3 +} diff --git a/packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/project.pbxproj b/packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/project.pbxproj index 1004b13303..e64e6bfc18 100644 --- a/packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/project.pbxproj +++ b/packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 4D7D8DD62DB14F18002C453E /* AnyCodable in Frameworks */ = {isa = PBXBuildFile; productRef = 4D7D8DD52DB14F18002C453E /* AnyCodable */; }; 4DDAED692D50D49B00A19B1F /* FormbricksSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DDAED602D50D49A00A19B1F /* FormbricksSDK.framework */; }; /* End PBXBuildFile section */ @@ -108,6 +109,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4D7D8DD62DB14F18002C453E /* AnyCodable in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -122,11 +124,19 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4D7D8DD42DB14F18002C453E /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; 4DDAED562D50D49A00A19B1F = { isa = PBXGroup; children = ( 4DDAED622D50D49A00A19B1F /* FormbricksSDK */, 4DDAED6C2D50D49B00A19B1F /* FormbricksSDKTests */, + 4D7D8DD42DB14F18002C453E /* Frameworks */, 4DDAED612D50D49A00A19B1F /* Products */, ); sourceTree = ""; @@ -171,6 +181,7 @@ ); name = FormbricksSDK; packageProductDependencies = ( + 4D7D8DD52DB14F18002C453E /* AnyCodable */, ); productName = FormbricksSDK; productReference = 4DDAED602D50D49A00A19B1F /* FormbricksSDK.framework */; @@ -227,6 +238,9 @@ ); mainGroup = 4DDAED562D50D49A00A19B1F; minimizedProjectReferenceProxies = 1; + packageReferences = ( + 4DA4A0952DB14E67007299C0 /* XCRemoteSwiftPackageReference "AnyCodable" */, + ); preferredProjectObjectVersion = 77; productRefGroup = 4DDAED612D50D49A00A19B1F /* Products */; projectDirPath = ""; @@ -536,6 +550,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 4DA4A0952DB14E67007299C0 /* XCRemoteSwiftPackageReference "AnyCodable" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Flight-School/AnyCodable"; + requirement = { + kind = exactVersion; + version = 0.6.7; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 4D7D8DD52DB14F18002C453E /* AnyCodable */ = { + isa = XCSwiftPackageProductDependency; + package = 4DA4A0952DB14E67007299C0 /* XCRemoteSwiftPackageReference "AnyCodable" */; + productName = AnyCodable; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 4DDAED572D50D49A00A19B1F /* Project object */; } diff --git a/packages/ios/FormbricksSDK/FormbricksSDK/Formbricks.swift b/packages/ios/FormbricksSDK/FormbricksSDK/Formbricks.swift index 26ab43931f..101f89875d 100644 --- a/packages/ios/FormbricksSDK/FormbricksSDK/Formbricks.swift +++ b/packages/ios/FormbricksSDK/FormbricksSDK/Formbricks.swift @@ -91,6 +91,11 @@ import Network 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) } diff --git a/packages/ios/FormbricksSDK/FormbricksSDK/Helpers/AnyCodable/AnyCodable.swift b/packages/ios/FormbricksSDK/FormbricksSDK/Helpers/AnyCodable/AnyCodable.swift deleted file mode 100644 index 28e949ccf9..0000000000 --- a/packages/ios/FormbricksSDK/FormbricksSDK/Helpers/AnyCodable/AnyCodable.swift +++ /dev/null @@ -1,147 +0,0 @@ -// https://github.com/Flight-School/AnyCodable/blob/master/Sources/AnyCodable/AnyCodable.swift - -import Foundation -/** - A type-erased `Codable` value. - - The `AnyCodable` type forwards encoding and decoding responsibilities - to an underlying value, hiding its specific underlying type. - - You can encode or decode mixed-type values in dictionaries - and other collections that require `Encodable` or `Decodable` conformance - by declaring their contained type to be `AnyCodable`. - - - SeeAlso: `AnyEncodable` - - SeeAlso: `AnyDecodable` - */ -struct AnyCodable: Codable { - public let value: Any - - public init(_ value: T?) { - self.value = value ?? () - } -} - -extension AnyCodable: AnyEncodableProtocol, AnyDecodableProtocol {} - -extension AnyCodable: Equatable { - public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool { - switch (lhs.value, rhs.value) { - case is (Void, Void): - return true - case let (lhs as Bool, rhs as Bool): - return lhs == rhs - case let (lhs as Int, rhs as Int): - return lhs == rhs - case let (lhs as Int8, rhs as Int8): - return lhs == rhs - case let (lhs as Int16, rhs as Int16): - return lhs == rhs - case let (lhs as Int32, rhs as Int32): - return lhs == rhs - case let (lhs as Int64, rhs as Int64): - return lhs == rhs - case let (lhs as UInt, rhs as UInt): - return lhs == rhs - case let (lhs as UInt8, rhs as UInt8): - return lhs == rhs - case let (lhs as UInt16, rhs as UInt16): - return lhs == rhs - case let (lhs as UInt32, rhs as UInt32): - return lhs == rhs - case let (lhs as UInt64, rhs as UInt64): - return lhs == rhs - case let (lhs as Float, rhs as Float): - return lhs == rhs - case let (lhs as Double, rhs as Double): - return lhs == rhs - case let (lhs as String, rhs as String): - return lhs == rhs - case let (lhs as [String: AnyCodable], rhs as [String: AnyCodable]): - return lhs == rhs - case let (lhs as [AnyCodable], rhs as [AnyCodable]): - return lhs == rhs - case let (lhs as [String: Any], rhs as [String: Any]): - return NSDictionary(dictionary: lhs) == NSDictionary(dictionary: rhs) - case let (lhs as [Any], rhs as [Any]): - return NSArray(array: lhs) == NSArray(array: rhs) - case is (NSNull, NSNull): - return true - default: - return false - } - } -} - -extension AnyCodable: CustomStringConvertible { - public var description: String { - switch value { - case is Void: - return String(describing: nil as Any?) - case let value as CustomStringConvertible: - return value.description - default: - return String(describing: value) - } - } -} - -extension AnyCodable: CustomDebugStringConvertible { - public var debugDescription: String { - if let value = value as? CustomDebugStringConvertible { - return "AnyCodable(\(value.debugDescription))" - } - return "AnyCodable(\(description))" - } -} - -extension AnyCodable: ExpressibleByNilLiteral {} -extension AnyCodable: ExpressibleByBooleanLiteral {} -extension AnyCodable: ExpressibleByIntegerLiteral {} -extension AnyCodable: ExpressibleByFloatLiteral {} -extension AnyCodable: ExpressibleByStringLiteral {} -extension AnyCodable: ExpressibleByStringInterpolation {} -extension AnyCodable: ExpressibleByArrayLiteral {} -extension AnyCodable: ExpressibleByDictionaryLiteral {} - - -extension AnyCodable: Hashable { - public func hash(into hasher: inout Hasher) { - switch value { - case let value as Bool: - hasher.combine(value) - case let value as Int: - hasher.combine(value) - case let value as Int8: - hasher.combine(value) - case let value as Int16: - hasher.combine(value) - case let value as Int32: - hasher.combine(value) - case let value as Int64: - hasher.combine(value) - case let value as UInt: - hasher.combine(value) - case let value as UInt8: - hasher.combine(value) - case let value as UInt16: - hasher.combine(value) - case let value as UInt32: - hasher.combine(value) - case let value as UInt64: - hasher.combine(value) - case let value as Float: - hasher.combine(value) - case let value as Double: - hasher.combine(value) - case let value as String: - hasher.combine(value) - case let value as [String: AnyCodable]: - hasher.combine(value) - case let value as [AnyCodable]: - hasher.combine(value) - default: - break - } - } -} diff --git a/packages/ios/FormbricksSDK/FormbricksSDK/Helpers/AnyCodable/AnyDecodable.swift b/packages/ios/FormbricksSDK/FormbricksSDK/Helpers/AnyCodable/AnyDecodable.swift deleted file mode 100644 index 9941c869f4..0000000000 --- a/packages/ios/FormbricksSDK/FormbricksSDK/Helpers/AnyCodable/AnyDecodable.swift +++ /dev/null @@ -1,189 +0,0 @@ -// https://github.com/Flight-School/AnyCodable/blob/master/Sources/AnyCodable/AnyCodable.swift - -#if canImport(Foundation) -import Foundation -#endif - -/** - A type-erased `Decodable` value. - - The `AnyDecodable` type forwards decoding responsibilities - to an underlying value, hiding its specific underlying type. - - You can decode mixed-type values in dictionaries - and other collections that require `Decodable` conformance - by declaring their contained type to be `AnyDecodable`: - - let json = """ - { - "boolean": true, - "integer": 42, - "double": 3.141592653589793, - "string": "string", - "array": [1, 2, 3], - "nested": { - "a": "alpha", - "b": "bravo", - "c": "charlie" - }, - "null": null - } - """.data(using: .utf8)! - - let decoder = JSONDecoder() - let dictionary = try! decoder.decode([String: AnyDecodable].self, from: json) - */ -struct AnyDecodable: Decodable { - public let value: Any - - public init(_ value: T?) { - self.value = value ?? () - } -} - -@usableFromInline -protocol AnyDecodableProtocol { - var value: Any { get } - init(_ value: T?) -} - -extension AnyDecodable: AnyDecodableProtocol {} - -extension AnyDecodableProtocol { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - if container.decodeNil() { - #if canImport(Foundation) - self.init(NSNull()) - #else - self.init(Optional.none) - #endif - } else if let bool = try? container.decode(Bool.self) { - self.init(bool) - } else if let int = try? container.decode(Int.self) { - self.init(int) - } else if let uint = try? container.decode(UInt.self) { - self.init(uint) - } else if let double = try? container.decode(Double.self) { - self.init(double) - } else if let string = try? container.decode(String.self) { - self.init(string) - } else if let array = try? container.decode([AnyDecodable].self) { - self.init(array.map { $0.value }) - } else if let dictionary = try? container.decode([String: AnyDecodable].self) { - self.init(dictionary.mapValues { $0.value }) - } else { - throw DecodingError.dataCorruptedError(in: container, debugDescription: "AnyDecodable value cannot be decoded") - } - } -} - -extension AnyDecodable: Equatable { - public static func == (lhs: AnyDecodable, rhs: AnyDecodable) -> Bool { - switch (lhs.value, rhs.value) { -#if canImport(Foundation) - case is (NSNull, NSNull), is (Void, Void): - return true -#endif - case let (lhs as Bool, rhs as Bool): - return lhs == rhs - case let (lhs as Int, rhs as Int): - return lhs == rhs - case let (lhs as Int8, rhs as Int8): - return lhs == rhs - case let (lhs as Int16, rhs as Int16): - return lhs == rhs - case let (lhs as Int32, rhs as Int32): - return lhs == rhs - case let (lhs as Int64, rhs as Int64): - return lhs == rhs - case let (lhs as UInt, rhs as UInt): - return lhs == rhs - case let (lhs as UInt8, rhs as UInt8): - return lhs == rhs - case let (lhs as UInt16, rhs as UInt16): - return lhs == rhs - case let (lhs as UInt32, rhs as UInt32): - return lhs == rhs - case let (lhs as UInt64, rhs as UInt64): - return lhs == rhs - case let (lhs as Float, rhs as Float): - return lhs == rhs - case let (lhs as Double, rhs as Double): - return lhs == rhs - case let (lhs as String, rhs as String): - return lhs == rhs - case let (lhs as [String: AnyDecodable], rhs as [String: AnyDecodable]): - return lhs == rhs - case let (lhs as [AnyDecodable], rhs as [AnyDecodable]): - return lhs == rhs - default: - return false - } - } -} - -extension AnyDecodable: CustomStringConvertible { - public var description: String { - switch value { - case is Void: - return String(describing: nil as Any?) - case let value as CustomStringConvertible: - return value.description - default: - return String(describing: value) - } - } -} - -extension AnyDecodable: CustomDebugStringConvertible { - public var debugDescription: String { - if let value = value as? CustomDebugStringConvertible { - return "AnyDecodable(\(value.debugDescription))" - } else { - return "AnyDecodable(\(description))" - } - } -} - -extension AnyDecodable: Hashable { - public func hash(into hasher: inout Hasher) { - switch value { - case let value as Bool: - hasher.combine(value) - case let value as Int: - hasher.combine(value) - case let value as Int8: - hasher.combine(value) - case let value as Int16: - hasher.combine(value) - case let value as Int32: - hasher.combine(value) - case let value as Int64: - hasher.combine(value) - case let value as UInt: - hasher.combine(value) - case let value as UInt8: - hasher.combine(value) - case let value as UInt16: - hasher.combine(value) - case let value as UInt32: - hasher.combine(value) - case let value as UInt64: - hasher.combine(value) - case let value as Float: - hasher.combine(value) - case let value as Double: - hasher.combine(value) - case let value as String: - hasher.combine(value) - case let value as [String: AnyDecodable]: - hasher.combine(value) - case let value as [AnyDecodable]: - hasher.combine(value) - default: - break - } - } -} diff --git a/packages/ios/FormbricksSDK/FormbricksSDK/Helpers/AnyCodable/AnyEncodable.swift b/packages/ios/FormbricksSDK/FormbricksSDK/Helpers/AnyCodable/AnyEncodable.swift deleted file mode 100644 index f891fec484..0000000000 --- a/packages/ios/FormbricksSDK/FormbricksSDK/Helpers/AnyCodable/AnyEncodable.swift +++ /dev/null @@ -1,292 +0,0 @@ -// https://github.com/Flight-School/AnyCodable/blob/master/Sources/AnyCodable/AnyCodable.swift - -#if canImport(Foundation) -import Foundation -#endif - -/** - A type-erased `Encodable` value. - - The `AnyEncodable` type forwards encoding responsibilities - to an underlying value, hiding its specific underlying type. - - You can encode mixed-type values in dictionaries - and other collections that require `Encodable` conformance - by declaring their contained type to be `AnyEncodable`: - - let dictionary: [String: AnyEncodable] = [ - "boolean": true, - "integer": 42, - "double": 3.141592653589793, - "string": "string", - "array": [1, 2, 3], - "nested": [ - "a": "alpha", - "b": "bravo", - "c": "charlie" - ], - "null": nil - ] - - let encoder = JSONEncoder() - let json = try! encoder.encode(dictionary) - */ -struct AnyEncodable: Encodable { - public let value: Any - - public init(_ value: T?) { - self.value = value ?? () - } -} - -@usableFromInline -protocol AnyEncodableProtocol { - var value: Any { get } - init(_ value: T?) -} - -extension AnyEncodable: AnyEncodableProtocol {} - -// MARK: - Encodable - -extension AnyEncodableProtocol { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - switch value { - #if canImport(Foundation) - case is NSNull: - try container.encodeNil() - #endif - case is Void: - try container.encodeNil() - case let bool as Bool: - try container.encode(bool) - case let int as Int: - try container.encode(int) - case let int8 as Int8: - try container.encode(int8) - case let int16 as Int16: - try container.encode(int16) - case let int32 as Int32: - try container.encode(int32) - case let int64 as Int64: - try container.encode(int64) - case let uint as UInt: - try container.encode(uint) - case let uint8 as UInt8: - try container.encode(uint8) - case let uint16 as UInt16: - try container.encode(uint16) - case let uint32 as UInt32: - try container.encode(uint32) - case let uint64 as UInt64: - try container.encode(uint64) - case let float as Float: - try container.encode(float) - case let double as Double: - try container.encode(double) - case let string as String: - try container.encode(string) - #if canImport(Foundation) - case let number as NSNumber: - try encode(nsnumber: number, into: &container) - case let date as Date: - try container.encode(date) - case let url as URL: - try container.encode(url) - #endif - case let array as [Any?]: - try container.encode(array.map { AnyEncodable($0) }) - case let dictionary as [String: Any?]: - try container.encode(dictionary.mapValues { AnyEncodable($0) }) - case let encodable as Encodable: - try encodable.encode(to: encoder) - default: - let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "AnyEncodable value cannot be encoded") - throw EncodingError.invalidValue(value, context) - } - } - - #if canImport(Foundation) - private func encode(nsnumber: NSNumber, into container: inout SingleValueEncodingContainer) throws { - switch Character(Unicode.Scalar(UInt8(nsnumber.objCType.pointee))) { - case "B": - try container.encode(nsnumber.boolValue) - case "c": - try container.encode(nsnumber.int8Value) - case "s": - try container.encode(nsnumber.int16Value) - case "i", "l": - try container.encode(nsnumber.int32Value) - case "q": - try container.encode(nsnumber.int64Value) - case "C": - try container.encode(nsnumber.uint8Value) - case "S": - try container.encode(nsnumber.uint16Value) - case "I", "L": - try container.encode(nsnumber.uint32Value) - case "Q": - try container.encode(nsnumber.uint64Value) - case "f": - try container.encode(nsnumber.floatValue) - case "d": - try container.encode(nsnumber.doubleValue) - default: - let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "NSNumber cannot be encoded because its type is not handled") - throw EncodingError.invalidValue(nsnumber, context) - } - } - #endif -} - -extension AnyEncodable: Equatable { - public static func == (lhs: AnyEncodable, rhs: AnyEncodable) -> Bool { - switch (lhs.value, rhs.value) { - case is (Void, Void): - return true - case let (lhs as Bool, rhs as Bool): - return lhs == rhs - case let (lhs as Int, rhs as Int): - return lhs == rhs - case let (lhs as Int8, rhs as Int8): - return lhs == rhs - case let (lhs as Int16, rhs as Int16): - return lhs == rhs - case let (lhs as Int32, rhs as Int32): - return lhs == rhs - case let (lhs as Int64, rhs as Int64): - return lhs == rhs - case let (lhs as UInt, rhs as UInt): - return lhs == rhs - case let (lhs as UInt8, rhs as UInt8): - return lhs == rhs - case let (lhs as UInt16, rhs as UInt16): - return lhs == rhs - case let (lhs as UInt32, rhs as UInt32): - return lhs == rhs - case let (lhs as UInt64, rhs as UInt64): - return lhs == rhs - case let (lhs as Float, rhs as Float): - return lhs == rhs - case let (lhs as Double, rhs as Double): - return lhs == rhs - case let (lhs as String, rhs as String): - return lhs == rhs - case let (lhs as [String: AnyEncodable], rhs as [String: AnyEncodable]): - return lhs == rhs - case let (lhs as [AnyEncodable], rhs as [AnyEncodable]): - return lhs == rhs - default: - return false - } - } -} - -extension AnyEncodable: CustomStringConvertible { - public var description: String { - switch value { - case is Void: - return String(describing: nil as Any?) - case let value as CustomStringConvertible: - return value.description - default: - return String(describing: value) - } - } -} - -extension AnyEncodable: CustomDebugStringConvertible { - public var debugDescription: String { - if let value = value as? CustomDebugStringConvertible { - return "AnyEncodable(\(value.debugDescription))" - } else { - return "AnyEncodable(\(description))" - } - } -} - -extension AnyEncodable: ExpressibleByNilLiteral {} -extension AnyEncodable: ExpressibleByBooleanLiteral {} -extension AnyEncodable: ExpressibleByIntegerLiteral {} -extension AnyEncodable: ExpressibleByFloatLiteral {} -extension AnyEncodable: ExpressibleByStringLiteral {} -extension AnyEncodable: ExpressibleByStringInterpolation {} -extension AnyEncodable: ExpressibleByArrayLiteral {} -extension AnyEncodable: ExpressibleByDictionaryLiteral {} - -extension AnyEncodableProtocol { - public init(nilLiteral _: ()) { - self.init(nil as Any?) - } - - public init(booleanLiteral value: Bool) { - self.init(value) - } - - public init(integerLiteral value: Int) { - self.init(value) - } - - public init(floatLiteral value: Double) { - self.init(value) - } - - public init(extendedGraphemeClusterLiteral value: String) { - self.init(value) - } - - public init(stringLiteral value: String) { - self.init(value) - } - - public init(arrayLiteral elements: Any...) { - self.init(elements) - } - - public init(dictionaryLiteral elements: (AnyHashable, Any)...) { - self.init([AnyHashable: Any](elements, uniquingKeysWith: { first, _ in first })) - } -} - -extension AnyEncodable: Hashable { - public func hash(into hasher: inout Hasher) { - switch value { - case let value as Bool: - hasher.combine(value) - case let value as Int: - hasher.combine(value) - case let value as Int8: - hasher.combine(value) - case let value as Int16: - hasher.combine(value) - case let value as Int32: - hasher.combine(value) - case let value as Int64: - hasher.combine(value) - case let value as UInt: - hasher.combine(value) - case let value as UInt8: - hasher.combine(value) - case let value as UInt16: - hasher.combine(value) - case let value as UInt32: - hasher.combine(value) - case let value as UInt64: - hasher.combine(value) - case let value as Float: - hasher.combine(value) - case let value as Double: - hasher.combine(value) - case let value as String: - hasher.combine(value) - case let value as [String: AnyEncodable]: - hasher.combine(value) - case let value as [AnyEncodable]: - hasher.combine(value) - default: - break - } - } -} diff --git a/packages/ios/FormbricksSDK/FormbricksSDK/Manager/UserManager.swift b/packages/ios/FormbricksSDK/FormbricksSDK/Manager/UserManager.swift index dccea0ac59..d679aa7716 100644 --- a/packages/ios/FormbricksSDK/FormbricksSDK/Manager/UserManager.swift +++ b/packages/ios/FormbricksSDK/FormbricksSDK/Manager/UserManager.swift @@ -95,6 +95,9 @@ final class UserManager: UserManagerSyncable { self?.lastDisplayedAt = userResponse.data.state?.data?.lastDisplayAt self?.expiresAt = userResponse.data.state?.expiresAt + let serverLanguage = userResponse.data.state?.data?.language + Formbricks.language = serverLanguage ?? "default" + self?.updateQueue?.reset() self?.surveyManager?.filterSurveys() self?.startSyncTimer() @@ -128,6 +131,7 @@ final class UserManager: UserManagerSyncable { backingResponses = nil backingLastDisplayedAt = nil backingExpiresAt = nil + Formbricks.language = "default" updateQueue?.reset() if isUserIdDefined { diff --git a/packages/ios/FormbricksSDK/FormbricksSDK/Model/User/UserStateDetails.swift b/packages/ios/FormbricksSDK/FormbricksSDK/Model/User/UserStateDetails.swift index d45f0cb791..52808141d3 100644 --- a/packages/ios/FormbricksSDK/FormbricksSDK/Model/User/UserStateDetails.swift +++ b/packages/ios/FormbricksSDK/FormbricksSDK/Model/User/UserStateDetails.swift @@ -7,4 +7,5 @@ struct UserStateDetails: Codable { let displays: [Display]? let responses: [String]? let lastDisplayAt: Date? + let language: String? } diff --git a/packages/ios/FormbricksSDK/FormbricksSDK/Networking/Base/APIClient.swift b/packages/ios/FormbricksSDK/FormbricksSDK/Networking/Base/APIClient.swift index e6b35dc294..b2f384f9e0 100644 --- a/packages/ios/FormbricksSDK/FormbricksSDK/Networking/Base/APIClient.swift +++ b/packages/ios/FormbricksSDK/FormbricksSDK/Networking/Base/APIClient.swift @@ -12,131 +12,124 @@ class APIClient: Operation, @unchecked Sendable { } override func main() { - guard let apiURL = request.baseURL, var baseUrlComponents = URLComponents(string: apiURL) else { + guard let finalURL = buildFinalURL() else { completion?(.failure(FormbricksSDKError(type: .sdkIsNotInitialized))) return } - baseUrlComponents.queryItems = request.queryParams?.map { URLQueryItem(name: $0.key, value: $0.value) } - - guard var finalURL = baseUrlComponents.url else { - completion?(.failure(FormbricksSDKError(type: .invalidAppUrl))) - return - } - - guard let requestEndPoint = setPathParams(request.requestEndPoint) else { - completion?(.failure(FormbricksSDKError(type: .sdkIsNotInitialized))) - return - } - - finalURL.appendPathComponent(requestEndPoint) - let urlRequest = createURLRequest(forURL: finalURL) + logRequest(urlRequest) - // LOG - var requestLogMessage = "\(request.requestType.rawValue) >>> " - if let urlString = urlRequest.url?.absoluteString { - requestLogMessage.append(urlString) - } - if let headers = urlRequest.allHTTPHeaderFields { - requestLogMessage.append("\nHeaders: \(headers)") - } - if let body = urlRequest.httpBody { - requestLogMessage.append("\nBody: \(String(data: body, encoding: .utf8) ?? "")") - } - - Formbricks.logger?.info(requestLogMessage) - - session.dataTask(with: urlRequest) { (data, response, error) in - if let httpStatus = (response as? HTTPURLResponse)?.status { - var responseLogMessage = "\(httpStatus.rawValue) <<< " - if let urlString = response?.url?.absoluteString { - responseLogMessage.append(urlString) - } - - if httpStatus.responseType == .success { - guard let data = data else { - self.completion?(.failure(FormbricksAPIClientError(type: .invalidResponse, statusCode: httpStatus.rawValue))) - return - } - if let responseString = String(data: data, encoding: .utf8) { - responseLogMessage.append("\n\(responseString)\n") - } - - do { - if Request.Response.self == VoidResponse.self { - Formbricks.logger?.info(responseLogMessage) - - if let response = VoidResponse() as? Request.Response { - self.completion?(.success(response)) - } else { - self.completion?(.failure(FormbricksAPIClientError(type: .invalidResponse))) - } - } else { - var body = try self.request.decoder.decode(Request.Response.self, from: data) - Formbricks.logger?.info(responseLogMessage) - - // We want to save the entire response dictionary for the environment response - if var environmentResponse = body as? EnvironmentResponse, - let jsonString = String(data: data, encoding: .utf8) { - environmentResponse.responseString = jsonString - body = environmentResponse as! Request.Response - } - - - self.completion?(.success(body)) - } - } - catch let DecodingError.dataCorrupted(context) { - responseLogMessage.append("Data corrupted \(context)\n") - Formbricks.logger?.error(responseLogMessage) - self.completion?(.failure(FormbricksAPIClientError(type: .invalidResponse, statusCode: httpStatus.rawValue))) - } - catch let DecodingError.keyNotFound(key, context) { - responseLogMessage.append("Key '\(key)' not found: \(context.debugDescription)\n") - responseLogMessage.append("codingPath: \(context.codingPath)") - Formbricks.logger?.error(responseLogMessage) - self.completion?(.failure(FormbricksAPIClientError(type: .invalidResponse, statusCode: httpStatus.rawValue))) - } - catch let DecodingError.valueNotFound(value, context) { - responseLogMessage.append("Value '\(value)' not found: \(context.debugDescription)\n") - responseLogMessage.append("codingPath: \(context.codingPath)") - Formbricks.logger?.error(responseLogMessage) - self.completion?(.failure(FormbricksAPIClientError(type: .invalidResponse, statusCode: httpStatus.rawValue))) - } - catch let DecodingError.typeMismatch(type, context) { - responseLogMessage.append("Type '\(type)' mismatch: \(context.debugDescription)\n") - responseLogMessage.append("codingPath: \(context.codingPath)") - Formbricks.logger?.error(responseLogMessage) - self.completion?(.failure(FormbricksAPIClientError(type: .invalidResponse, statusCode: httpStatus.rawValue))) - } - catch { - responseLogMessage.append("error: \(error.message)") - Formbricks.logger?.error(responseLogMessage) - self.completion?(.failure(FormbricksAPIClientError(type: .invalidResponse, statusCode: httpStatus.rawValue))) - } - } else { - if let error = error { - responseLogMessage.append("\nError: \(error.localizedDescription)") - Formbricks.logger?.error(responseLogMessage) - self.completion?(.failure(error)) - } else if let data = data, let apiError = try? self.request.decoder.decode(FormbricksAPIError.self, from: data) { - Formbricks.logger?.error("\(responseLogMessage)\n\(apiError.getDetailedErrorMessage())") - self.completion?(.failure(apiError)) - } else { - let error = FormbricksAPIClientError(type: .responseError, statusCode: httpStatus.rawValue) - Formbricks.logger?.error("\(responseLogMessage)\n\(error.message)") - self.completion?(.failure(error)) - } - } - } - else { - let error = FormbricksAPIClientError(type: .invalidResponse) - Formbricks.logger?.error("ERROR \(error.message)") - self.completion?(.failure(error)) - } + session.dataTask(with: urlRequest) { data, response, error in + self.processResponse(data: data, response: response, error: error) }.resume() } + + private func buildFinalURL() -> URL? { + guard let apiURL = request.baseURL, var components = URLComponents(string: apiURL) else { return nil } + + components.queryItems = request.queryParams?.map { URLQueryItem(name: $0.key, value: $0.value) } + + guard var url = components.url, let path = setPathParams(request.requestEndPoint) else { return nil } + + url.appendPathComponent(path) + return url + } + + private func processResponse(data: Data?, response: URLResponse?, error: Error?) { + guard let httpStatus = (response as? HTTPURLResponse)?.status else { + let error = FormbricksAPIClientError(type: .invalidResponse) + Formbricks.logger?.error("ERROR \(error.message)") + completion?(.failure(error)) + return + } + + var message = "\(httpStatus.rawValue) <<< \(response?.url?.absoluteString ?? "")" + + if httpStatus.responseType == .success { + handleSuccessResponse(data: data, statusCode: httpStatus.rawValue, message: &message) + } else { + handleFailureResponse(data: data, error: error, statusCode: httpStatus.rawValue, message: message) + } + } + + private func handleSuccessResponse(data: Data?, statusCode: Int, message: inout String) { + guard let data = data else { + completion?(.failure(FormbricksAPIClientError(type: .invalidResponse, statusCode: statusCode))) + return + } + + if let responseString = String(data: data, encoding: .utf8) { + message.append("\n\(responseString)\n") + } + + do { + if Request.Response.self == VoidResponse.self { + Formbricks.logger?.info(message) + 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 + } + Formbricks.logger?.info(message) + completion?(.success(body)) + } + } catch { + handleDecodingError(error, message: &message, statusCode: statusCode) + } + } + + private func handleFailureResponse(data: Data?, error: Error?, statusCode: Int, message: String) { + var log = message + + if let error = error { + log.append("\nError: \(error.localizedDescription)") + Formbricks.logger?.error(log) + completion?(.failure(error)) + } else if let data = data, let apiError = try? request.decoder.decode(FormbricksAPIError.self, from: data) { + Formbricks.logger?.error("\(log)\n\(apiError.getDetailedErrorMessage())") + completion?(.failure(apiError)) + } else { + let error = FormbricksAPIClientError(type: .responseError, statusCode: statusCode) + Formbricks.logger?.error("\(log)\n\(error.message)") + completion?(.failure(error)) + } + } + + private func handleDecodingError(_ error: Error, message: inout String, statusCode: Int) { + switch error { + case let DecodingError.dataCorrupted(context): + message.append("Data corrupted: \(context)") + case let DecodingError.keyNotFound(key, context): + message.append("Key '\(key)' not found: \(context.debugDescription)\ncodingPath: \(context.codingPath)") + case let DecodingError.valueNotFound(value, context): + message.append("Value '\(value)' not found: \(context.debugDescription)\ncodingPath: \(context.codingPath)") + case let DecodingError.typeMismatch(type, context): + message.append("Type '\(type)' mismatch: \(context.debugDescription)\ncodingPath: \(context.codingPath)") + default: + message.append("Error: \(error.localizedDescription)") + } + + Formbricks.logger?.error(message) + completion?(.failure(FormbricksAPIClientError(type: .invalidResponse, statusCode: statusCode))) + } + + private func logRequest(_ request: URLRequest) { + var message = "\(request.httpMethod ?? "") >>> \(request.url?.absoluteString ?? "")" + + if let headers = request.allHTTPHeaderFields { + message.append("\nHeaders: \(headers)") + } + + if let body = request.httpBody, let bodyString = String(data: body, encoding: .utf8) { + message.append("\nBody: \(bodyString)") + } + + Formbricks.logger?.info(message) + } + } private extension APIClient { diff --git a/packages/ios/FormbricksSDK/FormbricksSDK/Networking/Queue/UpdateQueue.swift b/packages/ios/FormbricksSDK/FormbricksSDK/Networking/Queue/UpdateQueue.swift index 5d1ea197cb..bb847037dd 100644 --- a/packages/ios/FormbricksSDK/FormbricksSDK/Networking/Queue/UpdateQueue.swift +++ b/packages/ios/FormbricksSDK/FormbricksSDK/Networking/Queue/UpdateQueue.swift @@ -61,6 +61,7 @@ final class UpdateQueue { } else { // If no userId, just update locally without API call Formbricks.logger?.debug("UpdateQueue - updating language locally: \(language)") + return } startDebounceTimer() diff --git a/packages/ios/FormbricksSDK/FormbricksSDK/WebView/SurveyWebView.swift b/packages/ios/FormbricksSDK/FormbricksSDK/WebView/SurveyWebView.swift index e7e02711b5..7865da02c8 100644 --- a/packages/ios/FormbricksSDK/FormbricksSDK/WebView/SurveyWebView.swift +++ b/packages/ios/FormbricksSDK/FormbricksSDK/WebView/SurveyWebView.swift @@ -54,15 +54,19 @@ struct SurveyWebView: UIViewRepresentable { func clean() { HTTPCookieStorage.shared.removeCookies(since: Date.distantPast) WKWebsiteDataStore.default().fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in - records.forEach { record in - WKWebsiteDataStore.default().removeData(ofTypes: record.dataTypes, for: [record], completionHandler: { - /* - This completion handler is intentionally empty since we only need to - ensure the data is removed. No additional actions are required after - the website data has been cleared. - */ - }) - } + self.remove(records) + } + } + + private func remove(_ records: [WKWebsiteDataRecord]) { + records.forEach { record in + WKWebsiteDataStore.default().removeData(ofTypes: record.dataTypes, for: [record], completionHandler: { + /* + This completion handler is intentionally empty since we only need to + ensure the data is removed. No additional actions are required after + the website data has been cleared. + */ + }) } } }