diff --git a/android/consumer-rules.pro b/android/consumer-rules.pro index 3b6eeaa..8ca9ce8 100644 --- a/android/consumer-rules.pro +++ b/android/consumer-rules.pro @@ -2,4 +2,6 @@ -keep class com.formbricks.android.Formbricks { *; } -keep class com.formbricks.android.helper.FormbricksConfig { *; } -keep class com.formbricks.android.model.error.SDKError { *; } +-keep class com.formbricks.android.model.user.AttributeValue { *; } +-keep class com.formbricks.android.model.user.AttributeValue$* { *; } -keep interface com.formbricks.android.FormbricksCallback { *; } \ No newline at end of file diff --git a/android/src/androidTest/assets/User.json b/android/src/androidTest/assets/User.json index 31cc23b..a41b6f9 100644 --- a/android/src/androidTest/assets/User.json +++ b/android/src/androidTest/assets/User.json @@ -10,6 +10,8 @@ "userId": "6CCCE716-6783-4D0F-8344-9C7DFA43D8F7" }, "expiresAt": "2035-03-06T10:59:32.359Z" - } + }, + "messages": ["User synced successfully"], + "errors": ["Unknown attribute key: invalidKey"] } } diff --git a/android/src/androidTest/java/com/formbricks/android/FormbricksInstrumentedTest.kt b/android/src/androidTest/java/com/formbricks/android/FormbricksInstrumentedTest.kt index 600a9fd..88cedb4 100644 --- a/android/src/androidTest/java/com/formbricks/android/FormbricksInstrumentedTest.kt +++ b/android/src/androidTest/java/com/formbricks/android/FormbricksInstrumentedTest.kt @@ -8,6 +8,8 @@ import com.formbricks.android.helper.FormbricksConfig import com.formbricks.android.logger.Logger import com.formbricks.android.manager.SurveyManager import com.formbricks.android.manager.UserManager +import com.formbricks.android.model.user.AttributeValue +import com.formbricks.android.network.queue.UpdateQueue import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotEquals @@ -37,8 +39,12 @@ class FormbricksInstrumentedTest { fun setUp() { val appContext = InstrumentationRegistry.getInstrumentation().targetContext Formbricks.applicationContext = appContext + Formbricks.isInitialized = false + Formbricks.language = "default" UserManager.logout() + UpdateQueue.reset() SurveyManager.environmentDataHolder = null + SurveyManager.filteredSurveys.clear() FormbricksApi.service = MockFormbricksApiService() } @@ -57,7 +63,7 @@ class FormbricksInstrumentedTest { // Use methods before init should have no effect Formbricks.setUserId("userId") Formbricks.setLanguage("de") - Formbricks.setAttributes(mapOf("testA" to "testB")) + Formbricks.setAttributes(mapOf("testA" to AttributeValue.string("testB"))) Formbricks.setAttribute("test", "testKey") assertNull(UserManager.userId) assertEquals("default", Formbricks.language) @@ -73,7 +79,7 @@ class FormbricksInstrumentedTest { waitForSeconds(1) // Should be ignored, becuase we don't have user ID yet - Formbricks.setAttributes(mapOf("testA" to "testB")) + Formbricks.setAttributes(mapOf("testA" to AttributeValue.string("testB"))) Formbricks.setAttribute("test", "testKey") assertNull(UserManager.userId) @@ -174,6 +180,187 @@ class FormbricksInstrumentedTest { assertTrue(SurveyManager.isShowingSurvey) } + @Test + fun testSetAttributesWithUserId() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build()) + waitForSeconds(1) + + // Set userId first, then set attributes - exercises UpdateQueue.setAttributes with a valid userId + Formbricks.setUserId(userId) + waitForSeconds(2) + assertEquals(userId, UserManager.userId) + + Formbricks.setAttributes(mapOf( + "plan" to AttributeValue.string("premium"), + "score" to AttributeValue.number(99.5) + )) + waitForSeconds(1) + + // User should still be synced + assertEquals(userId, UserManager.userId) + } + + @Test + fun testAddAttributeWithUserId() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build()) + waitForSeconds(1) + + // Set userId first, then add attributes - exercises UpdateQueue.addAttribute with a valid userId + Formbricks.setUserId(userId) + waitForSeconds(2) + assertEquals(userId, UserManager.userId) + + Formbricks.setAttribute("John", "name") + Formbricks.setAttribute(42.0, "age") + Formbricks.setAttribute(99, "level") + waitForSeconds(1) + + assertEquals(userId, UserManager.userId) + } + + @Test + fun testSetLanguageWithUserId() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build()) + waitForSeconds(1) + + // Set userId first, then set language - exercises the if-branch in UpdateQueue.setLanguage + Formbricks.setUserId(userId) + waitForSeconds(2) + assertEquals(userId, UserManager.userId) + + Formbricks.setLanguage("de") + waitForSeconds(1) + + assertEquals("de", Formbricks.language) + assertEquals(userId, UserManager.userId) + } + + @Test + fun testSetUserIdSameValueIsNoOp() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build()) + waitForSeconds(1) + + Formbricks.setUserId(userId) + waitForSeconds(2) + assertEquals(userId, UserManager.userId) + + // Same userId again — should be a no-op + Formbricks.setUserId(userId) + assertEquals(userId, UserManager.userId) + } + + @Test + fun testSetUserIdDifferentValueOverridesPrevious() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build()) + waitForSeconds(1) + + Formbricks.setUserId(userId) + waitForSeconds(2) + assertEquals(userId, UserManager.userId) + assertNotNull(UserManager.expiresAt) + + // Different userId — should clean up previous state and re-sync + // (Previously this would error and return without doing anything) + val newUserId = "NEW-USER-ID-12345" + Formbricks.setUserId(newUserId) + + // Verify that logout was called: expiresAt should be cleared immediately + assertNull("expiresAt should be cleared by logout", UserManager.expiresAt) + + // After sync completes, the mock returns the hardcoded userId from User.json, + // so we just verify the SDK is still functional (sync completed without errors) + waitForSeconds(2) + assertNotNull("userId should be set after re-sync", UserManager.userId) + } + + @Test + fun testLogoutWithoutUserIdDoesNotError() { + // Mark SDK as initialized without triggering async operations + Formbricks.isInitialized = true + + // Logout without ever setting a userId — should not crash + assertNull(UserManager.userId) + Formbricks.logout() + assertNull(UserManager.userId) + } + + @Test + fun testSyncUserSetsLanguageFromResponse() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build()) + waitForSeconds(1) + + assertEquals("default", Formbricks.language) + + // Override the mock to return a user response that includes a language + val mockService = FormbricksApi.service as MockFormbricksApiService + val originalUser = mockService.user + mockService.user = originalUser.copy( + data = originalUser.data.copy( + state = originalUser.data.state.copy( + data = originalUser.data.state.data.copy( + language = "fr" + ) + ) + ) + ) + + // setUserId triggers syncUser, which should pick up the language from the response + Formbricks.setUserId(userId) + waitForSeconds(2) + + assertEquals(userId, UserManager.userId) + assertEquals("fr", Formbricks.language) + } + + @Test + fun testSyncUserCatchBlockOnApiError() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build()) + waitForSeconds(1) + + // Enable error mode so postUser returns a failure, exercising the catch block + (FormbricksApi.service as MockFormbricksApiService).isErrorResponseNeeded = true + + Formbricks.setUserId(userId) + waitForSeconds(2) + + // The sync should have failed gracefully — userId should not be set from the response + assertNull(UserManager.expiresAt) + } + + @Test + fun testLogoutClearsAllUserState() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build()) + waitForSeconds(1) + + // Set up a user so we have state to clear + Formbricks.setUserId(userId) + waitForSeconds(2) + assertEquals(userId, UserManager.userId) + assertNotNull(UserManager.expiresAt) + assertNotNull(UserManager.contactId) + + // Set language to something other than default + Formbricks.setLanguage("de") + assertEquals("de", Formbricks.language) + + // Logout should clear all user state + UserManager.logout() + + assertNull(UserManager.userId) + assertNull(UserManager.contactId) + assertNull(UserManager.expiresAt) + assertNull(UserManager.lastDisplayedAt) + assertEquals("default", Formbricks.language) + } + private fun waitForSeconds(seconds: Long) { val latch = CountDownLatch(1) latch.await(seconds, TimeUnit.SECONDS) diff --git a/android/src/androidTest/java/com/formbricks/android/MockFormbricksApiService.kt b/android/src/androidTest/java/com/formbricks/android/MockFormbricksApiService.kt index ea09895..f8cfe79 100644 --- a/android/src/androidTest/java/com/formbricks/android/MockFormbricksApiService.kt +++ b/android/src/androidTest/java/com/formbricks/android/MockFormbricksApiService.kt @@ -12,7 +12,7 @@ import com.formbricks.android.model.error.SDKError class MockFormbricksApiService: FormbricksApiService() { private val gson = Gson() private val environment: EnvironmentResponse - private val user: UserResponse + internal var user: UserResponse var isErrorResponseNeeded = false init { diff --git a/android/src/main/java/com/formbricks/android/Formbricks.kt b/android/src/main/java/com/formbricks/android/Formbricks.kt index 3d3704e..64e7674 100644 --- a/android/src/main/java/com/formbricks/android/Formbricks.kt +++ b/android/src/main/java/com/formbricks/android/Formbricks.kt @@ -11,8 +11,13 @@ import com.formbricks.android.logger.Logger import com.formbricks.android.manager.SurveyManager import com.formbricks.android.manager.UserManager import com.formbricks.android.model.error.SDKError +import com.formbricks.android.model.user.AttributeValue import com.formbricks.android.webview.FormbricksFragment import java.lang.RuntimeException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone @Keep object Formbricks { @@ -52,6 +57,7 @@ object Formbricks { return } + // Validate HTTPS URL if (!config.appUrl.startsWith("https://", ignoreCase = true)) { val error = RuntimeException("Only HTTPS URLs are allowed for security reasons. HTTP URLs are not permitted. Provided URL: ${config.appUrl}") @@ -59,6 +65,7 @@ object Formbricks { return } + applicationContext = context appUrl = config.appUrl @@ -68,7 +75,10 @@ object Formbricks { config.userId?.let { UserManager.set(it) } config.attributes?.let { UserManager.setAttributes(it) } - config.attributes?.get("language")?.let { UserManager.setLanguage(it) } + config.attributes?.get("language")?.stringValue?.let { + UserManager.setLanguage(it) + language = it + } FormbricksApi.initialize() SurveyManager.refreshEnvironmentIfNeeded(force = forceRefresh) @@ -79,6 +89,11 @@ object Formbricks { /** * Sets the user id for the current user with the given [String]. + * + * - If the same userId is already set, this is a no-op. + * - If a different userId is already set, the previous user state is cleaned up first + * before setting the new userId. + * * The SDK must be initialized before calling this method. * * ``` @@ -93,21 +108,28 @@ object Formbricks { return } - if(UserManager.userId != null) { - val error = RuntimeException("A userId is already set ${UserManager.userId} - please call logout first before setting a new one") - Logger.e(error) + // If the same userId is already set, no-op + val existing = UserManager.userId + if (existing != null && existing == userId) { + Logger.d("UserId is already set to the same value, skipping") return } + // If a different userId is set, clean up the previous user state first + if (existing != null && existing.isNotEmpty()) { + Logger.d("Different userId is being set, cleaning up previous user state") + UserManager.logout() + } + UserManager.set(userId) } /** - * Adds an attribute for the current user with the given [String] value and [String] key. + * Adds a string attribute for the current user. * The SDK must be initialized before calling this method. * * ``` - * Formbricks.setAttribute("my_attribute", "key") + * Formbricks.setAttribute("John", "name") * ``` * */ @@ -117,19 +139,86 @@ object Formbricks { Logger.e(error) return } - UserManager.addAttribute(attribute, key) + UserManager.addAttribute(AttributeValue.string(attribute), key) } /** - * Sets the user attributes for the current user with the given [Map] of [String] values and [String] keys. + * Adds a numeric attribute for the current user. + * The SDK must be initialized before calling this method. + * + * ``` + * Formbricks.setAttribute(42.0, "age") + * ``` + * + */ + fun setAttribute(attribute: Double, key: String) { + if (!isInitialized) { + val error = SDKError.sdkIsNotInitialized + Logger.e(error) + return + } + UserManager.addAttribute(AttributeValue.number(attribute), key) + } + + /** + * Adds an integer attribute for the current user. + * The value is converted to a [Double] internally. + * The SDK must be initialized before calling this method. + * + * ``` + * Formbricks.setAttribute(42, "age") + * ``` + * + */ + fun setAttribute(attribute: Int, key: String) { + setAttribute(attribute.toDouble(), key) + } + + /** + * Adds a date attribute for the current user. + * The date is converted to an ISO 8601 string. The backend will detect the format and treat it as a date type. + * The SDK must be initialized before calling this method. + * + * ``` + * Formbricks.setAttribute(Date(), "signupDate") + * ``` + * + */ + fun setAttribute(attribute: Date, key: String) { + if (!isInitialized) { + val error = SDKError.sdkIsNotInitialized + Logger.e(error) + return + } + val iso8601Format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + } + UserManager.addAttribute(AttributeValue.string(iso8601Format.format(attribute)), key) + } + + /** + * Sets the user attributes for the current user. + * + * Attribute types are determined by the value: + * - String values -> string attribute + * - Number values -> number attribute + * - Use ISO 8601 date strings for date attributes + * + * On first write to a new attribute, the type is set based on the value type. + * On subsequent writes, the value must match the existing attribute type. + * * The SDK must be initialized before calling this method. * * ``` - * Formbricks.setAttributes(mapOf(Pair("key", "my_attribute"))) + * Formbricks.setAttributes(mapOf( + * "name" to AttributeValue.string("John"), + * "age" to AttributeValue.number(30.0), + * "score" to AttributeValue.number(9.5) + * )) * ``` * */ - fun setAttributes(attributes: Map) { + fun setAttributes(attributes: Map) { if (!isInitialized) { val error = SDKError.sdkIsNotInitialized Logger.e(error) @@ -235,4 +324,4 @@ object Formbricks { val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) } -} \ No newline at end of file +} diff --git a/android/src/main/java/com/formbricks/android/api/FormbricksApi.kt b/android/src/main/java/com/formbricks/android/api/FormbricksApi.kt index 712c118..d762bfc 100644 --- a/android/src/main/java/com/formbricks/android/api/FormbricksApi.kt +++ b/android/src/main/java/com/formbricks/android/api/FormbricksApi.kt @@ -2,6 +2,7 @@ package com.formbricks.android.api import com.formbricks.android.Formbricks import com.formbricks.android.model.environment.EnvironmentDataHolder +import com.formbricks.android.model.user.AttributeValue import com.formbricks.android.model.user.PostUserBody import com.formbricks.android.model.user.UserResponse import com.formbricks.android.network.FormbricksApiService @@ -45,7 +46,7 @@ object FormbricksApi { } } - suspend fun postUser(userId: String, attributes: Map?): Result = withContext(Dispatchers.IO) { + suspend fun postUser(userId: String, attributes: Map?): Result = withContext(Dispatchers.IO) { retryApiCall { try { val result = service.postUser(Formbricks.environmentId, PostUserBody.create(userId, attributes)).getOrThrow() diff --git a/android/src/main/java/com/formbricks/android/helper/FormbricksConfig.kt b/android/src/main/java/com/formbricks/android/helper/FormbricksConfig.kt index fff2a09..dac20da 100644 --- a/android/src/main/java/com/formbricks/android/helper/FormbricksConfig.kt +++ b/android/src/main/java/com/formbricks/android/helper/FormbricksConfig.kt @@ -2,6 +2,7 @@ package com.formbricks.android.helper import androidx.annotation.Keep import androidx.fragment.app.FragmentManager +import com.formbricks.android.model.user.AttributeValue /** * Configuration options for the SDK @@ -13,13 +14,13 @@ class FormbricksConfig private constructor( val appUrl: String, val environmentId: String, val userId: String?, - val attributes: Map?, + val attributes: Map?, val loggingEnabled: Boolean, val fragmentManager: FragmentManager? ) { class Builder(private val appUrl: String, private val environmentId: String) { private var userId: String? = null - private var attributes: MutableMap = mutableMapOf() + private var attributes: MutableMap = mutableMapOf() private var loggingEnabled = false private var fragmentManager: FragmentManager? = null @@ -28,13 +29,38 @@ class FormbricksConfig private constructor( return this } - fun setAttributes(attributes: MutableMap): Builder { + /** + * Sets the attributes for the Builder object. + * + * ```kotlin + * .setAttributes(mutableMapOf( + * "name" to AttributeValue.string("John"), + * "age" to AttributeValue.number(30.0) + * )) + * ``` + */ + fun setAttributes(attributes: MutableMap): Builder { this.attributes = attributes return this } + /** + * Sets the attributes for the Builder object using string values. + * + * ```kotlin + * .setStringAttributes(mutableMapOf("name" to "John", "plan" to "free")) + * ``` + */ + fun setStringAttributes(attributes: MutableMap): Builder { + this.attributes = attributes.mapValues { AttributeValue.string(it.value) }.toMutableMap() + return this + } + + /** + * Adds a string attribute to the Builder object. + */ fun addAttribute(attribute: String, key: String): Builder { - this.attributes[key] = attribute + this.attributes[key] = AttributeValue.string(attribute) return this } @@ -59,4 +85,4 @@ class FormbricksConfig private constructor( ) } } -} \ No newline at end of file +} diff --git a/android/src/main/java/com/formbricks/android/manager/SurveyManager.kt b/android/src/main/java/com/formbricks/android/manager/SurveyManager.kt index d015508..9475db6 100644 --- a/android/src/main/java/com/formbricks/android/manager/SurveyManager.kt +++ b/android/src/main/java/com/formbricks/android/manager/SurveyManager.kt @@ -151,7 +151,7 @@ object SurveyManager { val codeActionClasses = actionClasses.filter { it.type == "code" } val actionClass = codeActionClasses.firstOrNull { it.key == action } if (actionClass == null) { - val error = RuntimeException("\"$action\" action unknown. Please add this action in Formbricks first in order to use it in your code.") + val error = RuntimeException("Action with identifier '$action' is unknown. Please add this action in Formbricks in order to use it via the SDK action tracking.") Logger.e(error) return } diff --git a/android/src/main/java/com/formbricks/android/manager/UserManager.kt b/android/src/main/java/com/formbricks/android/manager/UserManager.kt index b3a4f95..83ae473 100644 --- a/android/src/main/java/com/formbricks/android/manager/UserManager.kt +++ b/android/src/main/java/com/formbricks/android/manager/UserManager.kt @@ -9,6 +9,7 @@ import com.formbricks.android.extensions.guard import com.formbricks.android.extensions.lastDisplayAt import com.formbricks.android.logger.Logger import com.formbricks.android.model.error.SDKError +import com.formbricks.android.model.user.AttributeValue import com.formbricks.android.model.user.Display import com.formbricks.android.network.queue.UpdateQueue import com.google.gson.Gson @@ -57,7 +58,7 @@ object UserManager { * @param attribute * @param key */ - fun addAttribute(attribute: String, key: String) { + fun addAttribute(attribute: AttributeValue, key: String) { UpdateQueue.addAttribute(key, attribute) } @@ -66,7 +67,7 @@ object UserManager { * * @param attributes */ - fun setAttributes(attributes: Map) { + fun setAttributes(attributes: Map) { UpdateQueue.setAttributes(attributes) } @@ -126,7 +127,7 @@ object UserManager { * @param id * @param attributes */ - fun syncUser(id: String, attributes: Map? = null) { + fun syncUser(id: String, attributes: Map? = null) { CoroutineScope(Dispatchers.IO).launch { try { val userResponse = FormbricksApi.postUser(id, attributes).getOrThrow() @@ -139,10 +140,20 @@ object UserManager { expiresAt = userResponse.data.state.expiresAt() val languageFromUserResponse = userResponse.data.state.data.language - if(languageFromUserResponse != null) { + if (languageFromUserResponse != null) { Formbricks.language = languageFromUserResponse } + // Log errors (always visible) - e.g., invalid attribute keys, type mismatches + userResponse.data.errors?.forEach { error -> + Logger.e(RuntimeException(error)) + } + + // Log informational messages (debug only) + userResponse.data.messages?.forEach { message -> + Logger.d("User update message: $message") + } + UpdateQueue.reset() SurveyManager.filterSurveys() startSyncTimer() @@ -157,12 +168,7 @@ object UserManager { * Logs out the user and clears the user state. */ fun logout() { - val isUserIdDefined = userId != null - - if (!isUserIdDefined) { - val error = SDKError.noUserIdSetError - Logger.e(error) - } + Logger.d("Logging out and cleaning user state") prefManager.edit().apply { remove(CONTACT_ID_KEY) @@ -186,10 +192,6 @@ object UserManager { UpdateQueue.reset() SurveyManager.filterSurveys() - - if(isUserIdDefined) { - Logger.d("User logged out successfully!") - } } private fun startSyncTimer() { diff --git a/android/src/main/java/com/formbricks/android/model/user/AttributeValue.kt b/android/src/main/java/com/formbricks/android/model/user/AttributeValue.kt new file mode 100644 index 0000000..fed4198 --- /dev/null +++ b/android/src/main/java/com/formbricks/android/model/user/AttributeValue.kt @@ -0,0 +1,75 @@ +package com.formbricks.android.model.user + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import com.google.gson.JsonPrimitive +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer +import com.google.gson.annotations.JsonAdapter +import java.lang.reflect.Type + +/** + * Represents a user attribute value that can be a string or number. + * + * Attribute types are determined by the value type: + * - String values -> string attribute + * - Number values -> number attribute + * - Use ISO 8601 date strings for date attributes + * + * On first write to a new attribute, the type is set based on the value type. + * On subsequent writes, the value must match the existing attribute type. + * + * ```kotlin + * val attributes = mapOf( + * "name" to AttributeValue.StringValue("John"), + * "age" to AttributeValue.NumberValue(30.0), + * "score" to AttributeValue.NumberValue(9.5) + * ) + * ``` + */ +@JsonAdapter(AttributeValueAdapter::class) +sealed class AttributeValue { + data class StringValue(val value: String) : AttributeValue() + data class NumberValue(val value: Double) : AttributeValue() + + /** The string representation of this attribute value, if it is a string. */ + val stringValue: String? + get() = (this as? StringValue)?.value + + /** The numeric representation of this attribute value, if it is a number. */ + val numberValue: Double? + get() = (this as? NumberValue)?.value + + companion object { + fun string(value: String): AttributeValue = StringValue(value) + fun number(value: Double): AttributeValue = NumberValue(value) + } +} + +/** + * Gson adapter for [AttributeValue] that serializes/deserializes as the raw primitive value. + * - `StringValue("hello")` serializes as `"hello"` + * - `NumberValue(42.0)` serializes as `42.0` + */ +class AttributeValueAdapter : JsonSerializer, JsonDeserializer { + override fun serialize(src: AttributeValue, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { + return when (src) { + is AttributeValue.StringValue -> JsonPrimitive(src.value) + is AttributeValue.NumberValue -> JsonPrimitive(src.value) + } + } + + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): AttributeValue { + if (json.isJsonPrimitive) { + val primitive = json.asJsonPrimitive + return when { + primitive.isNumber -> AttributeValue.NumberValue(primitive.asDouble) + primitive.isString -> AttributeValue.StringValue(primitive.asString) + else -> throw JsonParseException("Expected String or Number for AttributeValue") + } + } + throw JsonParseException("Expected String or Number for AttributeValue") + } +} diff --git a/android/src/main/java/com/formbricks/android/model/user/PostUserBody.kt b/android/src/main/java/com/formbricks/android/model/user/PostUserBody.kt index 26dd962..9998b82 100644 --- a/android/src/main/java/com/formbricks/android/model/user/PostUserBody.kt +++ b/android/src/main/java/com/formbricks/android/model/user/PostUserBody.kt @@ -4,10 +4,10 @@ import com.google.gson.annotations.SerializedName data class PostUserBody( @SerializedName("userId") val userId: String, - @SerializedName("attributes") val attributes: Map? + @SerializedName("attributes") val attributes: Map? ) { companion object { - fun create(userId: String, attributes: Map?): PostUserBody { + fun create(userId: String, attributes: Map?): PostUserBody { return PostUserBody(userId, attributes) } } diff --git a/android/src/main/java/com/formbricks/android/model/user/UserResponseData.kt b/android/src/main/java/com/formbricks/android/model/user/UserResponseData.kt index afff7db..1125707 100644 --- a/android/src/main/java/com/formbricks/android/model/user/UserResponseData.kt +++ b/android/src/main/java/com/formbricks/android/model/user/UserResponseData.kt @@ -3,5 +3,7 @@ package com.formbricks.android.model.user import com.google.gson.annotations.SerializedName data class UserResponseData( - @SerializedName("state") val state: UserState + @SerializedName("state") val state: UserState, + @SerializedName("messages") val messages: List? = null, + @SerializedName("errors") val errors: List? = null ) diff --git a/android/src/main/java/com/formbricks/android/network/queue/UpdateQueue.kt b/android/src/main/java/com/formbricks/android/network/queue/UpdateQueue.kt index 0e115cd..2005cdb 100644 --- a/android/src/main/java/com/formbricks/android/network/queue/UpdateQueue.kt +++ b/android/src/main/java/com/formbricks/android/network/queue/UpdateQueue.kt @@ -3,6 +3,7 @@ package com.formbricks.android.network.queue import com.formbricks.android.logger.Logger import com.formbricks.android.manager.UserManager import com.formbricks.android.model.error.SDKError +import com.formbricks.android.model.user.AttributeValue import java.util.* import kotlin.concurrent.timer @@ -15,7 +16,7 @@ object UpdateQueue { private const val DEBOUNCE_INTERVAL: Long = 500 // 500 ms private var userId: String? = null - private var attributes: MutableMap? = null + private var attributes: MutableMap? = null private var language: String? = null private var timer: Timer? = null @@ -24,12 +25,12 @@ object UpdateQueue { startDebounceTimer() } - fun setAttributes(attributes: Map) { + fun setAttributes(attributes: Map) { this.attributes = attributes.toMutableMap() startDebounceTimer() } - fun addAttribute(key: String, attribute: String) { + fun addAttribute(key: String, attribute: AttributeValue) { if (attributes == null) { attributes = mutableMapOf() } @@ -38,13 +39,13 @@ object UpdateQueue { } fun setLanguage(language: String) { - val effectiveUserId = userId ?: UserManager.userId + val effectiveUserId = userId ?: UserManager.userId - if(effectiveUserId != null) { - addAttribute("language", language) + if (effectiveUserId != null) { + addAttribute("language", AttributeValue.string(language)) startDebounceTimer() } else { - Logger.d("UpdateQueue - updating language locally: ${language}") + Logger.d("UpdateQueue - updating language locally: $language") return } } @@ -75,4 +76,4 @@ object UpdateQueue { Logger.d("UpdateQueue - commit() called on UpdateQueue with $effectiveUserId and $attributes") UserManager.syncUser(effectiveUserId, attributes) } -} \ No newline at end of file +}