Compare commits

...

10 Commits

Author SHA1 Message Date
pandeymangg
413e9575b6 Merge remote-tracking branch 'origin/main' into vaxi/mobile-sdk-sonar-fixes 2025-04-21 13:45:54 +05:30
pandeymangg
712c3a0aaf some fixes 2025-04-21 13:20:31 +05:30
pandeymangg
55eb43f94e Merge branch 'vaxi/mobile-sdk-sonar-fixes' of https://github.com/vaxi87/formbricks into vaxi/mobile-sdk-sonar-fixes 2025-04-21 11:50:15 +05:30
Peter Pesti-Varga
0982813fa7 revert unnecessary weak var 2025-04-21 07:27:45 +02:00
pandeymangg
403e337793 Merge remote-tracking branch 'origin/main' into vaxi/mobile-sdk-sonar-fixes 2025-04-18 19:17:11 +05:30
Peter Pesti-Varga
a8a299e4b8 address a comment 2025-04-18 14:27:53 +02:00
Peter Pesti-Varga
e1a39d4ed9 Merge branch 'main' into vaxi/mobile-sdk-sonar-fixes
# Conflicts:
#	packages/ios/Demo/Demo/AppDelegate.swift
#	packages/ios/FormbricksSDK/FormbricksSDK/Networking/Base/APIClient.swift
2025-04-18 14:19:59 +02:00
Peter Pesti-Varga
959e46d5a9 Merge branch 'main' into vaxi/mobile-sdk-sonar-fixes
# Conflicts:
#	packages/ios/Demo/Demo/AppDelegate.swift
#	packages/ios/FormbricksSDK/FormbricksSDK/Networking/Base/APIClient.swift
2025-04-18 14:19:41 +02:00
Peter Pesti-Varga
7b0efc21b9 revert default values 2025-04-18 14:16:01 +02:00
Peter Pesti-Varga
74d24f80d4 Address SonarQube code smells 2025-04-18 14:06:06 +02:00
14 changed files with 206 additions and 756 deletions

1
.gitignore vendored
View File

@@ -72,3 +72,4 @@ infra/terraform/.terraform/
# IntelliJ IDEA
/.idea/
/*.iml
packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/project.xcworkspace/xcuserdata

View File

@@ -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: "*" },

View File

@@ -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")
}
}
}
}

View File

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

View File

@@ -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 = "<group>";
};
4DDAED562D50D49A00A19B1F = {
isa = PBXGroup;
children = (
4DDAED622D50D49A00A19B1F /* FormbricksSDK */,
4DDAED6C2D50D49B00A19B1F /* FormbricksSDKTests */,
4D7D8DD42DB14F18002C453E /* Frameworks */,
4DDAED612D50D49A00A19B1F /* Products */,
);
sourceTree = "<group>";
@@ -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 */;
}

View File

@@ -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)
}

View File

@@ -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<T>(_ 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
}
}
}

View File

@@ -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<T>(_ value: T?) {
self.value = value ?? ()
}
}
@usableFromInline
protocol AnyDecodableProtocol {
var value: Any { get }
init<T>(_ 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<Self>.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
}
}
}

View File

@@ -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<T>(_ value: T?) {
self.value = value ?? ()
}
}
@usableFromInline
protocol AnyEncodableProtocol {
var value: Any { get }
init<T>(_ 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
}
}
}

View File

@@ -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 {

View File

@@ -7,4 +7,5 @@ struct UserStateDetails: Codable {
let displays: [Display]?
let responses: [String]?
let lastDisplayAt: Date?
let language: String?
}

View File

@@ -12,131 +12,124 @@ class APIClient<Request: CodableRequest>: 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 {

View File

@@ -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()

View File

@@ -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.
*/
})
}
}
}