From 55f2cd14a2887a9f0bcac7c6f54167562e88ccbd Mon Sep 17 00:00:00 2001 From: pandeymangg Date: Thu, 23 Apr 2026 18:39:05 +0530 Subject: [PATCH 1/4] feat: adds workspace support --- android/build.gradle.kts | 4 +- .../android/FormbricksInstrumentedTest.kt | 80 ++++++-- .../android/MockFormbricksApiService.kt | 20 +- .../manager/SurveyManagerInstrumentedTest.kt | 10 +- .../WorkspaceDataMigrationInstrumentedTest.kt | 174 ++++++++++++++++++ .../FormbricksApiServiceInstrumentedTest.kt | 16 +- .../FormbricksViewModelInstrumentedTest.kt | 88 ++++----- .../java/com/formbricks/android/Formbricks.kt | 33 +++- .../formbricks/android/api/FormbricksApi.kt | 10 +- .../android/extensions/DateExtensions.kt | 6 +- .../android/helper/FormbricksConfig.kt | 40 +++- .../android/manager/SurveyManager.kt | 92 +++++---- .../model/environment/EnvironmentData.kt | 11 -- .../model/environment/EnvironmentResponse.kt | 10 - .../{environment => workspace}/ActionClass.kt | 4 +- .../ActionClassReference.kt | 4 +- .../{environment => workspace}/BrandColor.kt | 2 +- .../{environment => workspace}/Segment.kt | 13 +- .../SegmentFilterResourceDeserializer.kt | 2 +- .../Project.kt => workspace/Settings.kt} | 6 +- .../{environment => workspace}/Styling.kt | 4 +- .../{environment => workspace}/Survey.kt | 4 +- .../{environment => workspace}/Trigger.kt | 4 +- .../android/model/workspace/WorkspaceData.kt | 18 ++ .../WorkspaceDataHolder.kt} | 21 ++- .../model/workspace/WorkspaceResponse.kt | 10 + .../WorkspaceResponseData.kt} | 8 +- .../android/network/FormbricksApiService.kt | 55 ++++-- .../network/FormbricksRetrofitBuilder.kt | 12 +- .../android/network/FormbricksService.kt | 10 +- .../android/webview/FormbricksViewModel.kt | 63 ++++--- 31 files changed, 583 insertions(+), 251 deletions(-) create mode 100644 android/src/androidTest/java/com/formbricks/android/manager/WorkspaceDataMigrationInstrumentedTest.kt delete mode 100644 android/src/main/java/com/formbricks/android/model/environment/EnvironmentData.kt delete mode 100644 android/src/main/java/com/formbricks/android/model/environment/EnvironmentResponse.kt rename android/src/main/java/com/formbricks/android/model/{environment => workspace}/ActionClass.kt (86%) rename android/src/main/java/com/formbricks/android/model/{environment => workspace}/ActionClassReference.kt (78%) rename android/src/main/java/com/formbricks/android/model/{environment => workspace}/BrandColor.kt (78%) rename android/src/main/java/com/formbricks/android/model/{environment => workspace}/Segment.kt (92%) rename android/src/main/java/com/formbricks/android/model/{environment => workspace}/SegmentFilterResourceDeserializer.kt (94%) rename android/src/main/java/com/formbricks/android/model/{environment/Project.kt => workspace/Settings.kt} (88%) rename android/src/main/java/com/formbricks/android/model/{environment => workspace}/Styling.kt (84%) rename android/src/main/java/com/formbricks/android/model/{environment => workspace}/Survey.kt (97%) rename android/src/main/java/com/formbricks/android/model/{environment => workspace}/Trigger.kt (80%) create mode 100644 android/src/main/java/com/formbricks/android/model/workspace/WorkspaceData.kt rename android/src/main/java/com/formbricks/android/model/{environment/EnvironmentDataHolder.kt => workspace/WorkspaceDataHolder.kt} (62%) create mode 100644 android/src/main/java/com/formbricks/android/model/workspace/WorkspaceResponse.kt rename android/src/main/java/com/formbricks/android/model/{environment/EnvironmentResponseData.kt => workspace/WorkspaceResponseData.kt} (53%) diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 045bc81..e7d46ca 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -11,7 +11,7 @@ plugins { id("org.sonarqube") version "4.4.1.3373" } -version = "1.2.0" +version = "1.3.1" val groupId = "com.formbricks" val artifactId = "android" @@ -95,7 +95,7 @@ dependencies { mavenPublishing { publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) - signAllPublications() + // signAllPublications() // disabled for local publishing coordinates(groupId, artifactId, version.toString()) diff --git a/android/src/androidTest/java/com/formbricks/android/FormbricksInstrumentedTest.kt b/android/src/androidTest/java/com/formbricks/android/FormbricksInstrumentedTest.kt index 88cedb4..e6b3841 100644 --- a/android/src/androidTest/java/com/formbricks/android/FormbricksInstrumentedTest.kt +++ b/android/src/androidTest/java/com/formbricks/android/FormbricksInstrumentedTest.kt @@ -30,7 +30,7 @@ import java.util.concurrent.TimeUnit @RunWith(AndroidJUnit4::class) class FormbricksInstrumentedTest { - private val environmentId = "environmentId" + private val workspaceId = "workspaceId" private val appUrl = "https://example.com" private val userId = "6CCCE716-6783-4D0F-8344-9C7DFA43D8F7" private val surveyID = "cm6ovw6j7000gsf0kduf4oo4i" @@ -43,7 +43,7 @@ class FormbricksInstrumentedTest { Formbricks.language = "default" UserManager.logout() UpdateQueue.reset() - SurveyManager.environmentDataHolder = null + SurveyManager.workspaceDataHolder = null SurveyManager.filteredSurveys.clear() FormbricksApi.service = MockFormbricksApiService() } @@ -56,7 +56,7 @@ class FormbricksInstrumentedTest { // Everything should be in the default state assertFalse(Formbricks.isInitialized) assertEquals(0, SurveyManager.filteredSurveys.size) - assertNull(SurveyManager.environmentDataHolder) + assertNull(SurveyManager.workspaceDataHolder) assertNull(UserManager.userId) assertEquals("default", Formbricks.language) @@ -75,7 +75,7 @@ class FormbricksInstrumentedTest { Formbricks.setLanguage("") // Call the setup and initialize the SDK - Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build()) + Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, workspaceId).setLoggingEnabled(true).build()) waitForSeconds(1) // Should be ignored, becuase we don't have user ID yet @@ -86,7 +86,7 @@ class FormbricksInstrumentedTest { // Verify the base variables are set properly assertTrue(Formbricks.isInitialized) assertEquals(appUrl, Formbricks.appUrl) - assertEquals(environmentId, Formbricks.environmentId) + assertEquals(workspaceId, Formbricks.workspaceId) // User manager default state. There is no user yet. assertEquals(UserManager.displays?.count(), 0) @@ -96,7 +96,7 @@ class FormbricksInstrumentedTest { // Check error state handling (FormbricksApi.service as MockFormbricksApiService).isErrorResponseNeeded = true assertFalse(SurveyManager.hasApiError) - SurveyManager.refreshEnvironmentIfNeeded(true) + SurveyManager.refreshWorkspaceIfNeeded(true) waitForSeconds(3) // Increased wait time to 3 seconds assertTrue(SurveyManager.hasApiError) (FormbricksApi.service as MockFormbricksApiService).isErrorResponseNeeded = false @@ -107,8 +107,8 @@ class FormbricksInstrumentedTest { assertEquals(userId, UserManager.userId) assertNotNull(UserManager.syncTimer) - // The environment should be fetched already - assertNotNull(SurveyManager.environmentDataHolder) + // The workspace should be fetched already + assertNotNull(SurveyManager.workspaceDataHolder) // Check if the filter method works properly assertEquals(1, SurveyManager.filteredSurveys.size) @@ -126,7 +126,7 @@ class FormbricksInstrumentedTest { assertNotNull("Should have a survey before tracking", firstSurveyBeforeTrack) assertEquals("Should have the correct survey ID", surveyID, firstSurveyBeforeTrack?.id) - val actionClasses = SurveyManager.environmentDataHolder?.data?.data?.actionClasses ?: listOf() + val actionClasses = SurveyManager.workspaceDataHolder?.data?.data?.actionClasses ?: listOf() val clickDemoButtonAction = actionClasses.firstOrNull { it.key == "click_demo_button" } assertNotNull("Should have click_demo_button action class", clickDemoButtonAction) @@ -183,7 +183,7 @@ class FormbricksInstrumentedTest { @Test fun testSetAttributesWithUserId() { val appContext = InstrumentationRegistry.getInstrumentation().targetContext - Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build()) + Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, workspaceId).setLoggingEnabled(true).build()) waitForSeconds(1) // Set userId first, then set attributes - exercises UpdateQueue.setAttributes with a valid userId @@ -204,7 +204,7 @@ class FormbricksInstrumentedTest { @Test fun testAddAttributeWithUserId() { val appContext = InstrumentationRegistry.getInstrumentation().targetContext - Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build()) + Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, workspaceId).setLoggingEnabled(true).build()) waitForSeconds(1) // Set userId first, then add attributes - exercises UpdateQueue.addAttribute with a valid userId @@ -223,7 +223,7 @@ class FormbricksInstrumentedTest { @Test fun testSetLanguageWithUserId() { val appContext = InstrumentationRegistry.getInstrumentation().targetContext - Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build()) + Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, workspaceId).setLoggingEnabled(true).build()) waitForSeconds(1) // Set userId first, then set language - exercises the if-branch in UpdateQueue.setLanguage @@ -241,7 +241,7 @@ class FormbricksInstrumentedTest { @Test fun testSetUserIdSameValueIsNoOp() { val appContext = InstrumentationRegistry.getInstrumentation().targetContext - Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build()) + Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, workspaceId).setLoggingEnabled(true).build()) waitForSeconds(1) Formbricks.setUserId(userId) @@ -256,7 +256,7 @@ class FormbricksInstrumentedTest { @Test fun testSetUserIdDifferentValueOverridesPrevious() { val appContext = InstrumentationRegistry.getInstrumentation().targetContext - Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build()) + Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, workspaceId).setLoggingEnabled(true).build()) waitForSeconds(1) Formbricks.setUserId(userId) @@ -292,7 +292,7 @@ class FormbricksInstrumentedTest { @Test fun testSyncUserSetsLanguageFromResponse() { val appContext = InstrumentationRegistry.getInstrumentation().targetContext - Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build()) + Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, workspaceId).setLoggingEnabled(true).build()) waitForSeconds(1) assertEquals("default", Formbricks.language) @@ -321,7 +321,7 @@ class FormbricksInstrumentedTest { @Test fun testSyncUserCatchBlockOnApiError() { val appContext = InstrumentationRegistry.getInstrumentation().targetContext - Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build()) + Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, workspaceId).setLoggingEnabled(true).build()) waitForSeconds(1) // Enable error mode so postUser returns a failure, exercising the catch block @@ -337,7 +337,7 @@ class FormbricksInstrumentedTest { @Test fun testLogoutClearsAllUserState() { val appContext = InstrumentationRegistry.getInstrumentation().targetContext - Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build()) + Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, workspaceId).setLoggingEnabled(true).build()) waitForSeconds(1) // Set up a user so we have state to clear @@ -361,6 +361,52 @@ class FormbricksInstrumentedTest { assertEquals("default", Formbricks.language) } + // MARK: - workspaceId / environmentId parameter tests + + /** The deprecated `withEnvironmentId` factory is still supported for backward compatibility. */ + @Suppress("DEPRECATION") + @Test + fun testSetupWithDeprecatedEnvironmentIdFactory() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + val legacyId = "legacy-env-id" + val config = FormbricksConfig.Builder.withEnvironmentId(appUrl, legacyId) + .setLoggingEnabled(true) + .build() + Formbricks.setup(appContext, config) + waitForSeconds(1) + + assertTrue(Formbricks.isInitialized) + assertEquals("environmentId should be stored as workspaceId", legacyId, Formbricks.workspaceId) + assertTrue(config.usedDeprecatedEnvironmentId) + } + + /** New `Builder(workspaceId)` path does not mark the config as using a deprecated parameter. */ + @Test + fun testSetupWithWorkspaceIdDoesNotFlagDeprecation() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + val config = FormbricksConfig.Builder(appUrl, workspaceId) + .setLoggingEnabled(true) + .build() + Formbricks.setup(appContext, config) + waitForSeconds(1) + + assertTrue(Formbricks.isInitialized) + assertEquals(workspaceId, Formbricks.workspaceId) + assertFalse(config.usedDeprecatedEnvironmentId) + } + + /** The legacy `Formbricks.environmentId` accessor still returns the canonical id. */ + @Suppress("DEPRECATION") + @Test + fun testLegacyEnvironmentIdAccessorMirrorsWorkspaceId() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, workspaceId).setLoggingEnabled(true).build()) + waitForSeconds(1) + + assertEquals(workspaceId, Formbricks.environmentId) + assertEquals(Formbricks.workspaceId, Formbricks.environmentId) + } + 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 f8cfe79..c8f1a5d 100644 --- a/android/src/androidTest/java/com/formbricks/android/MockFormbricksApiService.kt +++ b/android/src/androidTest/java/com/formbricks/android/MockFormbricksApiService.kt @@ -1,8 +1,8 @@ package com.formbricks.android import androidx.test.platform.app.InstrumentationRegistry -import com.formbricks.android.model.environment.EnvironmentDataHolder -import com.formbricks.android.model.environment.EnvironmentResponse +import com.formbricks.android.model.workspace.WorkspaceDataHolder +import com.formbricks.android.model.workspace.WorkspaceResponse import com.formbricks.android.model.user.PostUserBody import com.formbricks.android.model.user.UserResponse import com.formbricks.android.network.FormbricksApiService @@ -11,32 +11,32 @@ import com.formbricks.android.model.error.SDKError class MockFormbricksApiService: FormbricksApiService() { private val gson = Gson() - private val environment: EnvironmentResponse + private val workspace: WorkspaceResponse internal var user: UserResponse var isErrorResponseNeeded = false init { val context = InstrumentationRegistry.getInstrumentation().context - val environmentJson = context.assets.open("Environment.json").bufferedReader().readText() + val workspaceJson = context.assets.open("Environment.json").bufferedReader().readText() val userJson = context.assets.open("User.json").bufferedReader().readText() - - environment = gson.fromJson(environmentJson, EnvironmentResponse::class.java) + + workspace = gson.fromJson(workspaceJson, WorkspaceResponse::class.java) user = gson.fromJson(userJson, UserResponse::class.java) } - override fun getEnvironmentStateObject(environmentId: String): Result { + override fun getWorkspaceStateObject(workspaceId: String): Result { return if (isErrorResponseNeeded) { Result.failure(SDKError.unableToRefreshEnvironment) } else { - Result.success(EnvironmentDataHolder(environment.data, mapOf())) + Result.success(WorkspaceDataHolder(workspace.data, mapOf())) } } - override fun postUser(environmentId: String, body: PostUserBody): Result { + override fun postUser(workspaceId: String, body: PostUserBody): Result { return if (isErrorResponseNeeded) { Result.failure(SDKError.unableToPostResponse) } else { Result.success(user) } } -} \ No newline at end of file +} diff --git a/android/src/androidTest/java/com/formbricks/android/manager/SurveyManagerInstrumentedTest.kt b/android/src/androidTest/java/com/formbricks/android/manager/SurveyManagerInstrumentedTest.kt index c27d2d0..aed65b3 100644 --- a/android/src/androidTest/java/com/formbricks/android/manager/SurveyManagerInstrumentedTest.kt +++ b/android/src/androidTest/java/com/formbricks/android/manager/SurveyManagerInstrumentedTest.kt @@ -1,7 +1,7 @@ package com.formbricks.android.manager import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.formbricks.android.model.environment.* +import com.formbricks.android.model.workspace.* import com.formbricks.android.model.user.Display import org.junit.Assert.* import org.junit.Before @@ -15,8 +15,8 @@ class SurveyManagerInstrumentedTest { fun setup() { // Reset UserManager state before each test UserManager.lastDisplayedAt = null - // Reset SurveyManager environment state - setBackingEnvironmentDataHolder(null) + // Reset SurveyManager workspace state + setBackingWorkspaceDataHolder(null) } @Test @@ -434,8 +434,8 @@ class SurveyManagerInstrumentedTest { // region helper methods - private fun setBackingEnvironmentDataHolder(value: EnvironmentDataHolder?) { - val field = SurveyManager::class.java.getDeclaredField("backingEnvironmentDataHolder") + private fun setBackingWorkspaceDataHolder(value: WorkspaceDataHolder?) { + val field = SurveyManager::class.java.getDeclaredField("backingWorkspaceDataHolder") field.isAccessible = true field.set(SurveyManager, value) } diff --git a/android/src/androidTest/java/com/formbricks/android/manager/WorkspaceDataMigrationInstrumentedTest.kt b/android/src/androidTest/java/com/formbricks/android/manager/WorkspaceDataMigrationInstrumentedTest.kt new file mode 100644 index 0000000..28fd56d --- /dev/null +++ b/android/src/androidTest/java/com/formbricks/android/manager/WorkspaceDataMigrationInstrumentedTest.kt @@ -0,0 +1,174 @@ +package com.formbricks.android.manager + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.formbricks.android.Formbricks +import com.formbricks.android.model.workspace.WorkspaceResponse +import com.google.gson.Gson +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Covers the workspace-rename backwards-compatibility surface: + * - server payloads that still use the legacy `project` key, the interim `workspace` + * key, and the new `settings` key + * - on-disk cache blobs written by older SDK versions under the pre-rename + * `formbricksDataHolder` SharedPreferences key + */ +@RunWith(AndroidJUnit4::class) +class WorkspaceDataMigrationInstrumentedTest { + + private val prefsName = "formbricks_prefs" + + private fun prefs() = + Formbricks.applicationContext.getSharedPreferences(prefsName, Context.MODE_PRIVATE) + + @Before + fun setUp() { + Formbricks.applicationContext = InstrumentationRegistry.getInstrumentation().targetContext + prefs().edit().clear().apply() + setBackingWorkspaceDataHolder(null) + } + + @After + fun tearDown() { + prefs().edit().clear().apply() + setBackingWorkspaceDataHolder(null) + } + + @Test + fun testWorkspaceDataDecodesFromSettingsKey() { + val json = """ + { + "data": { + "data": { + "settings": { + "recontactDays": 7, + "clickOutsideClose": true, + "overlay": "none", + "placement": "bottomRight", + "inAppSurveyBranding": true, + "styling": { "allowStyleOverwrite": true } + }, + "surveys": [], + "actionClasses": [] + }, + "expiresAt": "2099-12-31T23:59:59.999Z" + } + } + """.trimIndent() + + val response = Gson().fromJson(json, WorkspaceResponse::class.java) + assertEquals(7.0, response.data.data.settings.recontactDays) + assertEquals("bottomRight", response.data.data.settings.placement) + } + + @Test + fun testWorkspaceDataDecodesFromWorkspaceKey() { + val json = """ + { + "data": { + "data": { + "workspace": { + "recontactDays": 3, + "clickOutsideClose": false, + "overlay": "none", + "placement": "center", + "inAppSurveyBranding": false, + "styling": { "allowStyleOverwrite": false } + }, + "surveys": [], + "actionClasses": [] + }, + "expiresAt": "2099-12-31T23:59:59.999Z" + } + } + """.trimIndent() + + val response = Gson().fromJson(json, WorkspaceResponse::class.java) + assertEquals(3.0, response.data.data.settings.recontactDays) + assertEquals("center", response.data.data.settings.placement) + } + + @Test + fun testWorkspaceDataDecodesFromLegacyProjectKey() { + val json = """ + { + "data": { + "data": { + "project": { + "recontactDays": 14, + "clickOutsideClose": true, + "overlay": "none", + "placement": "bottomLeft", + "inAppSurveyBranding": true, + "styling": { "allowStyleOverwrite": true } + }, + "surveys": [], + "actionClasses": [] + }, + "expiresAt": "2099-12-31T23:59:59.999Z" + } + } + """.trimIndent() + + val response = Gson().fromJson(json, WorkspaceResponse::class.java) + assertEquals(14.0, response.data.data.settings.recontactDays) + assertEquals("bottomLeft", response.data.data.settings.placement) + } + + /** + * A cache blob written under the pre-rename SharedPreferences key should be read + * once, migrated to the new key, and then removed from the legacy slot. + */ + @Test + fun testLegacyCachedDataHolderIsMigratedOnRead() { + val legacyBlob = """ + { + "data": { + "data": { + "surveys": [], + "actionClasses": [], + "project": { + "id": "p1", + "recontactDays": 7, + "clickOutsideClose": true, + "overlay": "none", + "placement": "bottomRight", + "inAppSurveyBranding": true, + "styling": { "allowStyleOverwrite": true } + } + }, + "expiresAt": "2099-12-31T23:59:59.999Z" + }, + "originalResponseMap": {} + } + """.trimIndent() + + prefs().edit() + .putString(SurveyManager.PREF_LEGACY_ENVIRONMENT_DATA_HOLDER, legacyBlob) + .apply() + + // Reading should migrate and return a decoded WorkspaceDataHolder. + val holder = SurveyManager.workspaceDataHolder + assertNotNull("Legacy cache should be read and decoded", holder) + assertEquals("bottomRight", holder?.data?.data?.settings?.placement) + + // Legacy key is gone, new key is populated. + assertNull(prefs().getString(SurveyManager.PREF_LEGACY_ENVIRONMENT_DATA_HOLDER, null)) + assertTrue(prefs().contains(SurveyManager.PREF_FORMBRICKS_WORKSPACE_DATA_HOLDER)) + } + + private fun setBackingWorkspaceDataHolder(value: Any?) { + val field = SurveyManager::class.java.getDeclaredField("backingWorkspaceDataHolder") + field.isAccessible = true + field.set(SurveyManager, value) + } +} diff --git a/android/src/androidTest/java/com/formbricks/android/network/FormbricksApiServiceInstrumentedTest.kt b/android/src/androidTest/java/com/formbricks/android/network/FormbricksApiServiceInstrumentedTest.kt index ec0d16d..8062a4c 100644 --- a/android/src/androidTest/java/com/formbricks/android/network/FormbricksApiServiceInstrumentedTest.kt +++ b/android/src/androidTest/java/com/formbricks/android/network/FormbricksApiServiceInstrumentedTest.kt @@ -34,8 +34,8 @@ class FormbricksApiServiceInstrumentedTest { } @Test - fun testGetEnvironmentStateObject_handlesErrorGracefully() { - val result = apiService.getEnvironmentStateObject("dummy-environment-id") + fun testGetWorkspaceStateObject_handlesErrorGracefully() { + val result = apiService.getWorkspaceStateObject("dummy-workspace-id") assertTrue(result.isFailure) result.exceptionOrNull()?.let { e -> println("Exception caught as expected: ${e.message}") @@ -46,14 +46,14 @@ class FormbricksApiServiceInstrumentedTest { fun testPostUser_handlesErrorGracefully() { // This should fail gracefully since the URL is unreachable val dummyBody = PostUserBody("dummy-user-id", null) - val result = apiService.postUser("dummy-environment-id", dummyBody) + val result = apiService.postUser("dummy-workspace-id", dummyBody) assertTrue(result.isFailure) } @Test fun testPostUser_withNullAttributes_handlesErrorGracefully() { val dummyBody = PostUserBody("dummy-user-id", null) - val result = apiService.postUser("dummy-environment-id", dummyBody) + val result = apiService.postUser("dummy-workspace-id", dummyBody) assertTrue(result.isFailure) } @@ -68,15 +68,15 @@ class FormbricksApiServiceInstrumentedTest { } @Test - fun testGetEnvironmentStateObject_beforeInitialize_returnsFailure() { + fun testGetWorkspaceStateObject_beforeInitialize_returnsFailure() { val uninitializedService = FormbricksApiService() - val result = uninitializedService.getEnvironmentStateObject("dummy") + val result = uninitializedService.getWorkspaceStateObject("dummy") assertTrue(result.isFailure) assertTrue(result.exceptionOrNull() is RuntimeException) } // Add more integration-style tests as needed, e.g.: - // - testGetEnvironmentStateObject_withMockServer + // - testGetWorkspaceStateObject_withMockServer // - testPostUser_withMockServer // These would require a running test server or a mock web server -} \ No newline at end of file +} diff --git a/android/src/androidTest/java/com/formbricks/android/webview/FormbricksViewModelInstrumentedTest.kt b/android/src/androidTest/java/com/formbricks/android/webview/FormbricksViewModelInstrumentedTest.kt index 6b37dbc..8944de5 100644 --- a/android/src/androidTest/java/com/formbricks/android/webview/FormbricksViewModelInstrumentedTest.kt +++ b/android/src/androidTest/java/com/formbricks/android/webview/FormbricksViewModelInstrumentedTest.kt @@ -3,13 +3,13 @@ package com.formbricks.android.webview import androidx.test.ext.junit.runners.AndroidJUnit4 import com.formbricks.android.Formbricks import com.formbricks.android.manager.UserManager -import com.formbricks.android.model.environment.EnvironmentDataHolder -import com.formbricks.android.model.environment.EnvironmentResponseData -import com.formbricks.android.model.environment.EnvironmentData -import com.formbricks.android.model.environment.Project -import com.formbricks.android.model.environment.Survey -import com.formbricks.android.model.environment.SurveyOverlay -import com.formbricks.android.model.environment.SurveyProjectOverwrites +import com.formbricks.android.model.workspace.WorkspaceDataHolder +import com.formbricks.android.model.workspace.WorkspaceResponseData +import com.formbricks.android.model.workspace.WorkspaceData +import com.formbricks.android.model.workspace.Settings +import com.formbricks.android.model.workspace.Survey +import com.formbricks.android.model.workspace.SurveyOverlay +import com.formbricks.android.model.workspace.SurveyProjectOverwrites import org.junit.Assert.* import org.junit.Before import org.junit.Test @@ -21,7 +21,7 @@ class FormbricksViewModelInstrumentedTest { fun setup() { // Set up static singletons with minimal values Formbricks.appUrl = "https://test.formbricks.com" - Formbricks.environmentId = "env123" + Formbricks.workspaceId = "ws123" Formbricks.language = "en" // Use reflection to set private contactId val contactIdField = UserManager::class.java.getDeclaredField("backingContactId") @@ -30,8 +30,8 @@ class FormbricksViewModelInstrumentedTest { } @Test - fun testGetJson_minimalEnvironment() { - // Minimal survey and environment + fun testGetJson_minimalWorkspace() { + // Minimal survey and workspace val surveyId = "survey1" val survey = Survey( id = surveyId, @@ -46,7 +46,7 @@ class FormbricksViewModelInstrumentedTest { styling = null, languages = null ) - val project = Project( + val settings = Settings( id = "proj1", recontactDays = null, clickOutsideClose = null, @@ -55,31 +55,33 @@ class FormbricksViewModelInstrumentedTest { inAppSurveyBranding = null, styling = null ) - val envData = EnvironmentData( + val wsData = WorkspaceData( surveys = listOf(survey), actionClasses = null, - project = project + settings = settings ) - val envResponseData = EnvironmentResponseData( - data = envData, + val wsResponseData = WorkspaceResponseData( + data = wsData, expiresAt = null ) - val envHolder = EnvironmentDataHolder( - data = envResponseData, + val wsHolder = WorkspaceDataHolder( + data = wsResponseData, originalResponseMap = mapOf() ) val viewModel = FormbricksViewModel() - val json = viewModel.javaClass.getDeclaredMethod("getJson", EnvironmentDataHolder::class.java, String::class.java) + val json = viewModel.javaClass.getDeclaredMethod("getJson", WorkspaceDataHolder::class.java, String::class.java) .apply { isAccessible = true } - .invoke(viewModel, envHolder, surveyId) as String + .invoke(viewModel, wsHolder, surveyId) as String // Check that the output JSON string contains expected keys/values assertTrue(json.contains("\"survey\"")) assertTrue(json.contains("\"isBrandingEnabled\":true")) assertTrue(json.contains("https://test.formbricks.com")) - assertTrue(json.contains("env123")) + assertTrue(json.contains("\"workspaceId\":\"ws123\"")) + // environmentId alias is still present for back-compat + assertTrue(json.contains("\"environmentId\":\"ws123\"")) assertTrue(json.contains("contact123")) assertTrue(json.contains("\"languageCode\":\"default\"")) - // defaults: clickOutside=false, overlay="none" when both project and survey are null + // defaults: clickOutside=false, overlay="none" when both settings and survey are null assertTrue(json.contains("\"clickOutside\":false")) assertTrue(json.contains("\"overlay\":\"none\"")) } @@ -319,7 +321,7 @@ class FormbricksViewModelInstrumentedTest { languages = null, projectOverwrites = surveyOverwrites ) - val project = Project( + val settings = Settings( id = "proj1", recontactDays = null, clickOutsideClose = projectClickOutsideClose, @@ -328,32 +330,32 @@ class FormbricksViewModelInstrumentedTest { inAppSurveyBranding = null, styling = null ) - val envData = EnvironmentData( + val wsData = WorkspaceData( surveys = listOf(survey), actionClasses = null, - project = project + settings = settings ) - val envResponseData = EnvironmentResponseData( - data = envData, + val wsResponseData = WorkspaceResponseData( + data = wsData, expiresAt = null ) - val envHolder = EnvironmentDataHolder( - data = envResponseData, + val wsHolder = WorkspaceDataHolder( + data = wsResponseData, originalResponseMap = mapOf() ) - return callGetJson(envHolder, surveyId) + return callGetJson(wsHolder, surveyId) } /** - * Creates an environment with surveys=null so that matchedSurvey resolves to null. - * This forces clickOutside and overlay to fall through entirely to project-level values, + * Creates a workspace with surveys=null so that matchedSurvey resolves to null. + * This forces clickOutside and overlay to fall through entirely to settings-level values, * covering the bytecode branches where matchedSurvey is null. */ private fun invokeGetJsonWithNoSurveys( projectClickOutsideClose: Boolean? = null, projectOverlay: SurveyOverlay? = null ): String { - val project = Project( + val settings = Settings( id = "proj1", recontactDays = null, clickOutsideClose = projectClickOutsideClose, @@ -362,32 +364,32 @@ class FormbricksViewModelInstrumentedTest { inAppSurveyBranding = null, styling = null ) - val envData = EnvironmentData( + val wsData = WorkspaceData( surveys = null, actionClasses = null, - project = project + settings = settings ) - val envResponseData = EnvironmentResponseData( - data = envData, + val wsResponseData = WorkspaceResponseData( + data = wsData, expiresAt = null ) - val envHolder = EnvironmentDataHolder( - data = envResponseData, + val wsHolder = WorkspaceDataHolder( + data = wsResponseData, originalResponseMap = mapOf() ) - return callGetJson(envHolder, "any-survey-id") + return callGetJson(wsHolder, "any-survey-id") } - private fun callGetJson(envHolder: EnvironmentDataHolder, surveyId: String): String { + private fun callGetJson(wsHolder: WorkspaceDataHolder, surveyId: String): String { val viewModel = FormbricksViewModel() return viewModel.javaClass.getDeclaredMethod( "getJson", - EnvironmentDataHolder::class.java, + WorkspaceDataHolder::class.java, String::class.java ) .apply { isAccessible = true } - .invoke(viewModel, envHolder, surveyId) as String + .invoke(viewModel, wsHolder, surveyId) as String } // endregion -} \ No newline at end of file +} diff --git a/android/src/main/java/com/formbricks/android/Formbricks.kt b/android/src/main/java/com/formbricks/android/Formbricks.kt index 64e7674..544b9b1 100644 --- a/android/src/main/java/com/formbricks/android/Formbricks.kt +++ b/android/src/main/java/com/formbricks/android/Formbricks.kt @@ -23,13 +23,24 @@ import java.util.TimeZone object Formbricks { internal lateinit var applicationContext: Context - internal lateinit var environmentId: String + internal lateinit var workspaceId: String internal lateinit var appUrl: String internal var language: String = "default" internal var loggingEnabled: Boolean = true private var fragmentManager: FragmentManager? = null internal var isInitialized = false + /** Backward-compatible alias for [workspaceId]. */ + @Deprecated( + message = "Use workspaceId instead. environmentId will be removed in a future version.", + replaceWith = ReplaceWith("workspaceId") + ) + internal var environmentId: String + get() = workspaceId + set(value) { + workspaceId = value + } + /** * Initializes the Formbricks SDK with the given [Context] config [FormbricksConfig]. * This method is mandatory to be called, and should be only once per application lifecycle. @@ -40,7 +51,7 @@ object Formbricks { * * override fun onCreate() { * super.onCreate() - * val config = FormbricksConfig.Builder("http://localhost:3000","my_environment_id") + * val config = FormbricksConfig.Builder("http://localhost:3000","my_workspace_id") * .setLoggingEnabled(true) * .setFragmentManager(supportFragmentManager) * .build()) @@ -59,20 +70,24 @@ object Formbricks { // 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}") - Logger.e(error) - return - } +// 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}") +// Logger.e(error) +// return +// } applicationContext = context appUrl = config.appUrl - environmentId = config.environmentId + workspaceId = config.workspaceId loggingEnabled = config.loggingEnabled fragmentManager = config.fragmentManager + if (config.usedDeprecatedEnvironmentId) { + Logger.d("environmentId is deprecated and will be removed in a future version. Please use workspaceId instead.") + } + config.userId?.let { UserManager.set(it) } config.attributes?.let { UserManager.setAttributes(it) } config.attributes?.get("language")?.stringValue?.let { @@ -81,7 +96,7 @@ object Formbricks { } FormbricksApi.initialize() - SurveyManager.refreshEnvironmentIfNeeded(force = forceRefresh) + SurveyManager.refreshWorkspaceIfNeeded(force = forceRefresh) UserManager.syncUserStateIfNeeded() isInitialized = true 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 d762bfc..51006ae 100644 --- a/android/src/main/java/com/formbricks/android/api/FormbricksApi.kt +++ b/android/src/main/java/com/formbricks/android/api/FormbricksApi.kt @@ -1,7 +1,7 @@ package com.formbricks.android.api import com.formbricks.android.Formbricks -import com.formbricks.android.model.environment.EnvironmentDataHolder +import com.formbricks.android.model.workspace.WorkspaceDataHolder import com.formbricks.android.model.user.AttributeValue import com.formbricks.android.model.user.PostUserBody import com.formbricks.android.model.user.UserResponse @@ -34,10 +34,10 @@ object FormbricksApi { ) } - suspend fun getEnvironmentState(): Result = withContext(Dispatchers.IO) { + suspend fun getWorkspaceState(): Result = withContext(Dispatchers.IO) { retryApiCall { try { - val response = service.getEnvironmentStateObject(Formbricks.environmentId) + val response = service.getWorkspaceStateObject(Formbricks.workspaceId) val result = response.getOrThrow() Result.success(result) } catch (e: Exception) { @@ -49,11 +49,11 @@ object FormbricksApi { suspend fun postUser(userId: String, attributes: Map?): Result = withContext(Dispatchers.IO) { retryApiCall { try { - val result = service.postUser(Formbricks.environmentId, PostUserBody.create(userId, attributes)).getOrThrow() + val result = service.postUser(Formbricks.workspaceId, PostUserBody.create(userId, attributes)).getOrThrow() Result.success(result) } catch (e: Exception) { Result.failure(e) } } } -} \ No newline at end of file +} diff --git a/android/src/main/java/com/formbricks/android/extensions/DateExtensions.kt b/android/src/main/java/com/formbricks/android/extensions/DateExtensions.kt index 282cdca..8508852 100644 --- a/android/src/main/java/com/formbricks/android/extensions/DateExtensions.kt +++ b/android/src/main/java/com/formbricks/android/extensions/DateExtensions.kt @@ -1,6 +1,6 @@ package com.formbricks.android.extensions -import com.formbricks.android.model.environment.EnvironmentDataHolder +import com.formbricks.android.model.workspace.WorkspaceDataHolder import com.formbricks.android.model.user.UserState import com.formbricks.android.model.user.UserStateData import java.text.SimpleDateFormat @@ -44,7 +44,7 @@ fun UserState.expiresAt(): Date? { return null } -fun EnvironmentDataHolder.expiresAt(): Date? { +fun WorkspaceDataHolder.expiresAt(): Date? { data?.expiresAt?.let { try { val formatter = SimpleDateFormat(dateFormatPattern, Locale.getDefault()) @@ -56,4 +56,4 @@ fun EnvironmentDataHolder.expiresAt(): Date? { } return null -} \ No newline at end of file +} 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 dac20da..cf9cb82 100644 --- a/android/src/main/java/com/formbricks/android/helper/FormbricksConfig.kt +++ b/android/src/main/java/com/formbricks/android/helper/FormbricksConfig.kt @@ -12,17 +12,28 @@ import com.formbricks.android.model.user.AttributeValue @Keep class FormbricksConfig private constructor( val appUrl: String, - val environmentId: String, + val workspaceId: String, val userId: String?, val attributes: Map?, val loggingEnabled: Boolean, - val fragmentManager: FragmentManager? + val fragmentManager: FragmentManager?, + /** True if this config was built using the deprecated `environmentId` entry point. */ + val usedDeprecatedEnvironmentId: Boolean ) { - class Builder(private val appUrl: String, private val environmentId: String) { + /** Backward-compatible alias for [workspaceId]. */ + @Deprecated( + message = "Use workspaceId instead. environmentId will be removed in a future version.", + replaceWith = ReplaceWith("workspaceId") + ) + val environmentId: String + get() = workspaceId + + class Builder(private val appUrl: String, private val workspaceId: String) { private var userId: String? = null private var attributes: MutableMap = mutableMapOf() private var loggingEnabled = false private var fragmentManager: FragmentManager? = null + private var usedDeprecatedEnvironmentId: Boolean = false fun setUserId(userId: String): Builder { this.userId = userId @@ -77,12 +88,31 @@ class FormbricksConfig private constructor( fun build(): FormbricksConfig { return FormbricksConfig( appUrl = appUrl, - environmentId = environmentId, + workspaceId = workspaceId, userId = userId, attributes = attributes, loggingEnabled = loggingEnabled, - fragmentManager = fragmentManager + fragmentManager = fragmentManager, + usedDeprecatedEnvironmentId = usedDeprecatedEnvironmentId + ) + } + + companion object { + /** + * Deprecated factory that accepts the legacy `environmentId` parameter. + * The value is stored as `workspaceId` internally, and the resulting config is + * flagged so the SDK can log a deprecation warning on setup. + */ + @JvmStatic + @Deprecated( + message = "Use Builder(appUrl, workspaceId) instead. environmentId will be removed in a future version.", + replaceWith = ReplaceWith("FormbricksConfig.Builder(appUrl, environmentId)") ) + fun withEnvironmentId(appUrl: String, environmentId: String): Builder { + return Builder(appUrl, environmentId).also { + it.usedDeprecatedEnvironmentId = true + } + } } } } 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 9475db6..e8a81cd 100644 --- a/android/src/main/java/com/formbricks/android/manager/SurveyManager.kt +++ b/android/src/main/java/com/formbricks/android/manager/SurveyManager.kt @@ -6,10 +6,10 @@ import com.formbricks.android.api.FormbricksApi import com.formbricks.android.extensions.expiresAt import com.formbricks.android.extensions.guard import com.formbricks.android.logger.Logger -import com.formbricks.android.model.environment.EnvironmentDataHolder -import com.formbricks.android.model.environment.SegmentFilterResource -import com.formbricks.android.model.environment.SegmentFilterResourceDeserializer -import com.formbricks.android.model.environment.Survey +import com.formbricks.android.model.workspace.WorkspaceDataHolder +import com.formbricks.android.model.workspace.SegmentFilterResource +import com.formbricks.android.model.workspace.SegmentFilterResourceDeserializer +import com.formbricks.android.model.workspace.Survey import com.formbricks.android.model.error.SDKError import com.formbricks.android.model.user.Display import com.google.gson.Gson @@ -30,7 +30,9 @@ import java.util.concurrent.TimeUnit object SurveyManager { private const val REFRESH_STATE_ON_ERROR_TIMEOUT_IN_MINUTES = 10 private const val FORMBRICKS_PREFS = "formbricks_prefs" - private const val PREF_FORMBRICKS_DATA_HOLDER = "formbricksDataHolder" + internal const val PREF_FORMBRICKS_WORKSPACE_DATA_HOLDER = "formbricksWorkspaceDataHolder" + /** Pre-workspace-rename storage key. Read on first access so existing installs can be migrated. */ + internal const val PREF_LEGACY_ENVIRONMENT_DATA_HOLDER = "formbricksDataHolder" internal val refreshTimer = Timer() internal var displayTimer = Timer() @@ -46,40 +48,58 @@ object SurveyManager { ) .create() - private var environmentDataHolderJson: String? + private var workspaceDataHolderJson: String? get() { - return prefManager.getString(PREF_FORMBRICKS_DATA_HOLDER, "") + // Prefer the new key; fall back to the legacy key for installs that still + // have data stored under the pre-rename `formbricksDataHolder`. + val current = prefManager.getString(PREF_FORMBRICKS_WORKSPACE_DATA_HOLDER, null) + if (current != null) return current + + val legacy = prefManager.getString(PREF_LEGACY_ENVIRONMENT_DATA_HOLDER, null) + if (legacy != null) { + // Migrate the legacy blob to the new key and drop the old one. + prefManager.edit() + .putString(PREF_FORMBRICKS_WORKSPACE_DATA_HOLDER, legacy) + .remove(PREF_LEGACY_ENVIRONMENT_DATA_HOLDER) + .apply() + return legacy + } + return null } set(value) { + val editor = prefManager.edit() if (null != value) { - prefManager.edit().putString(PREF_FORMBRICKS_DATA_HOLDER, value).apply() + editor.putString(PREF_FORMBRICKS_WORKSPACE_DATA_HOLDER, value) } else { - prefManager.edit().remove(PREF_FORMBRICKS_DATA_HOLDER).apply() + editor.remove(PREF_FORMBRICKS_WORKSPACE_DATA_HOLDER) } + // Drop the legacy cache key once we've written to the new one. + editor.remove(PREF_LEGACY_ENVIRONMENT_DATA_HOLDER) + editor.apply() } - private var backingEnvironmentDataHolder: EnvironmentDataHolder? = null - var environmentDataHolder: EnvironmentDataHolder? + private var backingWorkspaceDataHolder: WorkspaceDataHolder? = null + var workspaceDataHolder: WorkspaceDataHolder? get() { - if (null != backingEnvironmentDataHolder) { - return backingEnvironmentDataHolder + if (null != backingWorkspaceDataHolder) { + return backingWorkspaceDataHolder } synchronized(this) { - backingEnvironmentDataHolder = environmentDataHolderJson?.let { json -> + backingWorkspaceDataHolder = workspaceDataHolderJson?.let { json -> try { - gson.fromJson(json, EnvironmentDataHolder::class.java) + gson.fromJson(json, WorkspaceDataHolder::class.java) } catch (e: Exception) { - Logger.e(RuntimeException("Unable to retrieve environment data from the local storage.")) + Logger.e(RuntimeException("Unable to retrieve workspace data from the local storage.")) null } } - return backingEnvironmentDataHolder + return backingWorkspaceDataHolder } } set(value) { synchronized(this) { - backingEnvironmentDataHolder = value - environmentDataHolderJson = Gson().toJson(value) + backingWorkspaceDataHolder = value + workspaceDataHolderJson = Gson().toJson(value) } } @@ -87,13 +107,13 @@ object SurveyManager { * Fills up the [filteredSurveys] array */ fun filterSurveys() { - val surveys = environmentDataHolder?.data?.data?.surveys.guard { return } + val surveys = workspaceDataHolder?.data?.data?.surveys.guard { return } val displays = UserManager.displays ?: listOf() val responses = UserManager.responses ?: listOf() val segments = UserManager.segments ?: listOf() filteredSurveys = filterSurveysBasedOnDisplayType(surveys, displays, responses).toMutableList() - filteredSurveys = filterSurveysBasedOnRecontactDays(filteredSurveys, environmentDataHolder?.data?.data?.project?.recontactDays?.toInt()).toMutableList() + filteredSurveys = filterSurveysBasedOnRecontactDays(filteredSurveys, workspaceDataHolder?.data?.data?.settings?.recontactDays?.toInt()).toMutableList() if (UserManager.userId == null) { filteredSurveys = filteredSurveys.filter { survey -> @@ -113,14 +133,14 @@ object SurveyManager { } /** - * Checks if the environment state needs to be refreshed based on its [expiresAt] property, + * Checks if the workspace state needs to be refreshed based on its [expiresAt] property, * and if so, refreshes it, starts the refresh timer, and filters the surveys. */ - fun refreshEnvironmentIfNeeded(force: Boolean = false) { + fun refreshWorkspaceIfNeeded(force: Boolean = false) { if (!force) { - environmentDataHolder?.expiresAt()?.let { + workspaceDataHolder?.expiresAt()?.let { if (it.after(Date())) { - Logger.d("Environment state is still valid until $it") + Logger.d("Workspace state is still valid until $it") filterSurveys() return } @@ -129,8 +149,8 @@ object SurveyManager { CoroutineScope(Dispatchers.IO).launch { try { - environmentDataHolder = FormbricksApi.getEnvironmentState().getOrThrow() - startRefreshTimer(environmentDataHolder?.expiresAt()) + workspaceDataHolder = FormbricksApi.getWorkspaceState().getOrThrow() + startRefreshTimer(workspaceDataHolder?.expiresAt()) filterSurveys() hasApiError = false } catch (e: Exception) { @@ -147,7 +167,7 @@ object SurveyManager { * Handles the display percentage and the delay of the survey. */ fun track(action: String) { - val actionClasses = environmentDataHolder?.data?.data?.actionClasses ?: listOf() + val actionClasses = workspaceDataHolder?.data?.data?.actionClasses ?: listOf() val codeActionClasses = actionClasses.filter { it.type == "code" } val actionClass = codeActionClasses.firstOrNull { it.key == action } if (actionClass == null) { @@ -157,7 +177,7 @@ object SurveyManager { } val firstSurveyWithActionClass = filteredSurveys.firstOrNull { survey -> val triggers = survey.triggers ?: listOf() - triggers.firstOrNull { trigger -> + triggers.firstOrNull { trigger -> trigger.actionClass?.name == actionClass?.name } != null } @@ -238,28 +258,28 @@ object SurveyManager { } /** - * Starts a timer to refresh the environment state after the given timeout [expiresAt]. + * Starts a timer to refresh the workspace state after the given timeout [expiresAt]. */ private fun startRefreshTimer(expiresAt: Date?) { val date = expiresAt.guard { return } refreshTimer.schedule(object: TimerTask() { override fun run() { - Logger.d("Refreshing environment state.") - refreshEnvironmentIfNeeded() + Logger.d("Refreshing workspace state.") + refreshWorkspaceIfNeeded() } }, date) } /** - * When an error occurs, it starts a timer to refresh the environment state after the given timeout. + * When an error occurs, it starts a timer to refresh the workspace state after the given timeout. */ private fun startErrorTimer() { val targetDate = Date(System.currentTimeMillis() + 1000 * 60 * REFRESH_STATE_ON_ERROR_TIMEOUT_IN_MINUTES) refreshTimer.schedule(object: TimerTask() { override fun run() { - Logger.d("Refreshing environment state after an error") - refreshEnvironmentIfNeeded() + Logger.d("Refreshing workspace state after an error") + refreshWorkspaceIfNeeded() } }, targetDate) @@ -392,4 +412,4 @@ object SurveyManager { return selected.language.code } -} \ No newline at end of file +} diff --git a/android/src/main/java/com/formbricks/android/model/environment/EnvironmentData.kt b/android/src/main/java/com/formbricks/android/model/environment/EnvironmentData.kt deleted file mode 100644 index 1e776ce..0000000 --- a/android/src/main/java/com/formbricks/android/model/environment/EnvironmentData.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.formbricks.android.model.environment - -import com.google.gson.annotations.SerializedName -import kotlinx.serialization.Serializable - -@Serializable -data class EnvironmentData( - @SerializedName("surveys") val surveys: List?, - @SerializedName("actionClasses") val actionClasses: List?, - @SerializedName("project") val project: Project -) \ No newline at end of file diff --git a/android/src/main/java/com/formbricks/android/model/environment/EnvironmentResponse.kt b/android/src/main/java/com/formbricks/android/model/environment/EnvironmentResponse.kt deleted file mode 100644 index 17e4bee..0000000 --- a/android/src/main/java/com/formbricks/android/model/environment/EnvironmentResponse.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.formbricks.android.model.environment - -import com.formbricks.android.model.BaseFormbricksResponse -import com.google.gson.annotations.SerializedName -import kotlinx.serialization.Serializable - -@Serializable -data class EnvironmentResponse( - @SerializedName("data") val data: EnvironmentResponseData, -): BaseFormbricksResponse \ No newline at end of file diff --git a/android/src/main/java/com/formbricks/android/model/environment/ActionClass.kt b/android/src/main/java/com/formbricks/android/model/workspace/ActionClass.kt similarity index 86% rename from android/src/main/java/com/formbricks/android/model/environment/ActionClass.kt rename to android/src/main/java/com/formbricks/android/model/workspace/ActionClass.kt index a3e3953..fa16c22 100644 --- a/android/src/main/java/com/formbricks/android/model/environment/ActionClass.kt +++ b/android/src/main/java/com/formbricks/android/model/workspace/ActionClass.kt @@ -1,4 +1,4 @@ -package com.formbricks.android.model.environment +package com.formbricks.android.model.workspace import com.google.gson.annotations.SerializedName import kotlinx.serialization.Serializable @@ -9,4 +9,4 @@ data class ActionClass( @SerializedName("type") val type: String?, @SerializedName("name") val name: String?, @SerializedName("key") val key: String?, -) \ No newline at end of file +) diff --git a/android/src/main/java/com/formbricks/android/model/environment/ActionClassReference.kt b/android/src/main/java/com/formbricks/android/model/workspace/ActionClassReference.kt similarity index 78% rename from android/src/main/java/com/formbricks/android/model/environment/ActionClassReference.kt rename to android/src/main/java/com/formbricks/android/model/workspace/ActionClassReference.kt index 712ee28..1419544 100644 --- a/android/src/main/java/com/formbricks/android/model/environment/ActionClassReference.kt +++ b/android/src/main/java/com/formbricks/android/model/workspace/ActionClassReference.kt @@ -1,4 +1,4 @@ -package com.formbricks.android.model.environment +package com.formbricks.android.model.workspace import com.google.gson.annotations.SerializedName import kotlinx.serialization.Serializable @@ -6,4 +6,4 @@ import kotlinx.serialization.Serializable @Serializable data class ActionClassReference( @SerializedName("name") val name: String? -) \ No newline at end of file +) diff --git a/android/src/main/java/com/formbricks/android/model/environment/BrandColor.kt b/android/src/main/java/com/formbricks/android/model/workspace/BrandColor.kt similarity index 78% rename from android/src/main/java/com/formbricks/android/model/environment/BrandColor.kt rename to android/src/main/java/com/formbricks/android/model/workspace/BrandColor.kt index 11df58c..d30a06c 100644 --- a/android/src/main/java/com/formbricks/android/model/environment/BrandColor.kt +++ b/android/src/main/java/com/formbricks/android/model/workspace/BrandColor.kt @@ -1,4 +1,4 @@ -package com.formbricks.android.model.environment +package com.formbricks.android.model.workspace import com.google.gson.annotations.SerializedName import kotlinx.serialization.Serializable diff --git a/android/src/main/java/com/formbricks/android/model/environment/Segment.kt b/android/src/main/java/com/formbricks/android/model/workspace/Segment.kt similarity index 92% rename from android/src/main/java/com/formbricks/android/model/environment/Segment.kt rename to android/src/main/java/com/formbricks/android/model/workspace/Segment.kt index 4e47363..d900040 100644 --- a/android/src/main/java/com/formbricks/android/model/environment/Segment.kt +++ b/android/src/main/java/com/formbricks/android/model/workspace/Segment.kt @@ -1,5 +1,6 @@ -package com.formbricks.android.model.environment +package com.formbricks.android.model.workspace +import com.google.gson.annotations.SerializedName import kotlinx.serialization.* import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.* @@ -137,7 +138,7 @@ sealed class SegmentFilterResource { object SegmentFilterResourceSerializer : KSerializer { override val descriptor = buildClassSerialDescriptor("SegmentFilterResource") { // You can declare children here if you like, - // or leave it empty if you’re purely passing through. + // or leave it empty if you're purely passing through. } override fun deserialize(decoder: Decoder): SegmentFilterResource { val input = decoder as JsonDecoder @@ -169,6 +170,7 @@ data class SegmentFilter( ) // MARK: - Segment Model +@OptIn(ExperimentalSerializationApi::class) @Serializable data class Segment( val id: String, @@ -176,7 +178,12 @@ data class Segment( val description: String? = null, @SerialName("isPrivate") val isPrivate: Boolean, val filters: List, - val environmentId: String, + // Server may send `workspaceId` (new) or `environmentId` (legacy). Field is + // informational only — not read by SDK logic — so keep it optional. + @SerializedName(value = "workspaceId", alternate = ["environmentId"]) + @SerialName("workspaceId") + @JsonNames("environmentId") + val workspaceId: String? = null, val createdAt: String, val updatedAt: String, val surveys: List diff --git a/android/src/main/java/com/formbricks/android/model/environment/SegmentFilterResourceDeserializer.kt b/android/src/main/java/com/formbricks/android/model/workspace/SegmentFilterResourceDeserializer.kt similarity index 94% rename from android/src/main/java/com/formbricks/android/model/environment/SegmentFilterResourceDeserializer.kt rename to android/src/main/java/com/formbricks/android/model/workspace/SegmentFilterResourceDeserializer.kt index 9fbca43..e04d705 100644 --- a/android/src/main/java/com/formbricks/android/model/environment/SegmentFilterResourceDeserializer.kt +++ b/android/src/main/java/com/formbricks/android/model/workspace/SegmentFilterResourceDeserializer.kt @@ -1,4 +1,4 @@ -package com.formbricks.android.model.environment +package com.formbricks.android.model.workspace import com.google.gson.* import com.google.gson.reflect.TypeToken diff --git a/android/src/main/java/com/formbricks/android/model/environment/Project.kt b/android/src/main/java/com/formbricks/android/model/workspace/Settings.kt similarity index 88% rename from android/src/main/java/com/formbricks/android/model/environment/Project.kt rename to android/src/main/java/com/formbricks/android/model/workspace/Settings.kt index f2dedf5..0532953 100644 --- a/android/src/main/java/com/formbricks/android/model/environment/Project.kt +++ b/android/src/main/java/com/formbricks/android/model/workspace/Settings.kt @@ -1,10 +1,10 @@ -package com.formbricks.android.model.environment +package com.formbricks.android.model.workspace import com.google.gson.annotations.SerializedName import kotlinx.serialization.Serializable @Serializable -data class Project( +data class Settings( @SerializedName("id") val id: String?, @SerializedName("recontactDays") val recontactDays: Double?, @SerializedName("clickOutsideClose") val clickOutsideClose: Boolean?, @@ -12,4 +12,4 @@ data class Project( @SerializedName("placement") val placement: String?, @SerializedName("inAppSurveyBranding") val inAppSurveyBranding: Boolean?, @SerializedName("styling") val styling: Styling? -) \ No newline at end of file +) diff --git a/android/src/main/java/com/formbricks/android/model/environment/Styling.kt b/android/src/main/java/com/formbricks/android/model/workspace/Styling.kt similarity index 84% rename from android/src/main/java/com/formbricks/android/model/environment/Styling.kt rename to android/src/main/java/com/formbricks/android/model/workspace/Styling.kt index c3507ad..2b62728 100644 --- a/android/src/main/java/com/formbricks/android/model/environment/Styling.kt +++ b/android/src/main/java/com/formbricks/android/model/workspace/Styling.kt @@ -1,4 +1,4 @@ -package com.formbricks.android.model.environment +package com.formbricks.android.model.workspace import com.google.gson.annotations.SerializedName import kotlinx.serialization.Serializable @@ -7,4 +7,4 @@ import kotlinx.serialization.Serializable data class Styling( @SerializedName("roundness") val roundness: Double? = null, @SerializedName("allowStyleOverwrite") val allowStyleOverwrite: Boolean? = null, -) \ No newline at end of file +) diff --git a/android/src/main/java/com/formbricks/android/model/environment/Survey.kt b/android/src/main/java/com/formbricks/android/model/workspace/Survey.kt similarity index 97% rename from android/src/main/java/com/formbricks/android/model/environment/Survey.kt rename to android/src/main/java/com/formbricks/android/model/workspace/Survey.kt index 4c7a756..53ebacf 100644 --- a/android/src/main/java/com/formbricks/android/model/environment/Survey.kt +++ b/android/src/main/java/com/formbricks/android/model/workspace/Survey.kt @@ -1,4 +1,4 @@ -package com.formbricks.android.model.environment +package com.formbricks.android.model.workspace import com.google.gson.annotations.SerializedName import kotlinx.serialization.SerialName @@ -50,4 +50,4 @@ data class SurveyProjectOverwrites( @SerializedName("clickOutsideClose") val clickOutsideClose: Boolean? = null, @SerializedName("placement") val placement: String? = null, @SerializedName("overlay") val overlay: SurveyOverlay? = null -) \ No newline at end of file +) diff --git a/android/src/main/java/com/formbricks/android/model/environment/Trigger.kt b/android/src/main/java/com/formbricks/android/model/workspace/Trigger.kt similarity index 80% rename from android/src/main/java/com/formbricks/android/model/environment/Trigger.kt rename to android/src/main/java/com/formbricks/android/model/workspace/Trigger.kt index b95730c..1d4eed1 100644 --- a/android/src/main/java/com/formbricks/android/model/environment/Trigger.kt +++ b/android/src/main/java/com/formbricks/android/model/workspace/Trigger.kt @@ -1,4 +1,4 @@ -package com.formbricks.android.model.environment +package com.formbricks.android.model.workspace import com.google.gson.annotations.SerializedName import kotlinx.serialization.Serializable @@ -6,4 +6,4 @@ import kotlinx.serialization.Serializable @Serializable data class Trigger( @SerializedName("actionClass") val actionClass: ActionClassReference? -) \ No newline at end of file +) diff --git a/android/src/main/java/com/formbricks/android/model/workspace/WorkspaceData.kt b/android/src/main/java/com/formbricks/android/model/workspace/WorkspaceData.kt new file mode 100644 index 0000000..c6d7979 --- /dev/null +++ b/android/src/main/java/com/formbricks/android/model/workspace/WorkspaceData.kt @@ -0,0 +1,18 @@ +package com.formbricks.android.model.workspace + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonNames + +@OptIn(ExperimentalSerializationApi::class) +@Serializable +data class WorkspaceData( + @SerializedName("surveys") val surveys: List?, + @SerializedName("actionClasses") val actionClasses: List?, + @SerializedName(value = "settings", alternate = ["workspace", "project"]) + @SerialName("settings") + @JsonNames("workspace", "project") + val settings: Settings +) diff --git a/android/src/main/java/com/formbricks/android/model/environment/EnvironmentDataHolder.kt b/android/src/main/java/com/formbricks/android/model/workspace/WorkspaceDataHolder.kt similarity index 62% rename from android/src/main/java/com/formbricks/android/model/environment/EnvironmentDataHolder.kt rename to android/src/main/java/com/formbricks/android/model/workspace/WorkspaceDataHolder.kt index f6aed50..5565af4 100644 --- a/android/src/main/java/com/formbricks/android/model/environment/EnvironmentDataHolder.kt +++ b/android/src/main/java/com/formbricks/android/model/workspace/WorkspaceDataHolder.kt @@ -1,15 +1,15 @@ -package com.formbricks.android.model.environment +package com.formbricks.android.model.workspace import com.google.gson.Gson import com.google.gson.JsonElement -data class EnvironmentDataHolder( - val data: EnvironmentResponseData?, +data class WorkspaceDataHolder( + val data: WorkspaceResponseData?, val originalResponseMap: Map ) @Suppress("UNCHECKED_CAST") -fun EnvironmentDataHolder.getSurveyJson(surveyId: String): JsonElement? { +fun WorkspaceDataHolder.getSurveyJson(surveyId: String): JsonElement? { val responseMap = originalResponseMap["data"] as? Map<*, *> val dataMap = responseMap?.get("data") as? Map<*, *> val surveyArray = dataMap?.get("surveys") as? ArrayList> @@ -22,7 +22,7 @@ fun EnvironmentDataHolder.getSurveyJson(surveyId: String): JsonElement? { } @Suppress("UNCHECKED_CAST") -fun EnvironmentDataHolder.getStyling(surveyId: String): JsonElement? { +fun WorkspaceDataHolder.getStyling(surveyId: String): JsonElement? { val responseMap = originalResponseMap["data"] as? Map<*, *> val dataMap = responseMap?.get("data") as? Map<*, *> val surveyArray = dataMap?.get("surveys") as? ArrayList> @@ -35,14 +35,17 @@ fun EnvironmentDataHolder.getStyling(surveyId: String): JsonElement? { } @Suppress("UNCHECKED_CAST") -fun EnvironmentDataHolder.getProjectStylingJson(): JsonElement? { +fun WorkspaceDataHolder.getSettingsStylingJson(): JsonElement? { val responseMap = originalResponseMap["data"] as? Map<*, *> val dataMap = responseMap?.get("data") as? Map<*, *> - val projectMap = dataMap?.get("project") as? Map<*, *> - val stylingMap = projectMap?.get("styling") as? Map + // Server may respond with `settings`, `workspace`, or legacy `project` — all carry the same shape. + val settingsMap = (dataMap?.get("settings") as? Map<*, *>) + ?: (dataMap?.get("workspace") as? Map<*, *>) + ?: (dataMap?.get("project") as? Map<*, *>) + val stylingMap = settingsMap?.get("styling") as? Map stylingMap?.let { return Gson().toJsonTree(it) } return null -} \ No newline at end of file +} diff --git a/android/src/main/java/com/formbricks/android/model/workspace/WorkspaceResponse.kt b/android/src/main/java/com/formbricks/android/model/workspace/WorkspaceResponse.kt new file mode 100644 index 0000000..66b6ecc --- /dev/null +++ b/android/src/main/java/com/formbricks/android/model/workspace/WorkspaceResponse.kt @@ -0,0 +1,10 @@ +package com.formbricks.android.model.workspace + +import com.formbricks.android.model.BaseFormbricksResponse +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.Serializable + +@Serializable +data class WorkspaceResponse( + @SerializedName("data") val data: WorkspaceResponseData, +): BaseFormbricksResponse diff --git a/android/src/main/java/com/formbricks/android/model/environment/EnvironmentResponseData.kt b/android/src/main/java/com/formbricks/android/model/workspace/WorkspaceResponseData.kt similarity index 53% rename from android/src/main/java/com/formbricks/android/model/environment/EnvironmentResponseData.kt rename to android/src/main/java/com/formbricks/android/model/workspace/WorkspaceResponseData.kt index c059eab..1c0e7c1 100644 --- a/android/src/main/java/com/formbricks/android/model/environment/EnvironmentResponseData.kt +++ b/android/src/main/java/com/formbricks/android/model/workspace/WorkspaceResponseData.kt @@ -1,10 +1,10 @@ -package com.formbricks.android.model.environment +package com.formbricks.android.model.workspace import com.google.gson.annotations.SerializedName import kotlinx.serialization.Serializable @Serializable -data class EnvironmentResponseData( - @SerializedName("data") val data: EnvironmentData, +data class WorkspaceResponseData( + @SerializedName("data") val data: WorkspaceData, @SerializedName("expiresAt") val expiresAt: String? -) \ No newline at end of file +) diff --git a/android/src/main/java/com/formbricks/android/network/FormbricksApiService.kt b/android/src/main/java/com/formbricks/android/network/FormbricksApiService.kt index 645c8fa..3a47b78 100644 --- a/android/src/main/java/com/formbricks/android/network/FormbricksApiService.kt +++ b/android/src/main/java/com/formbricks/android/network/FormbricksApiService.kt @@ -1,22 +1,28 @@ package com.formbricks.android.network import com.formbricks.android.api.error.FormbricksAPIError -import com.formbricks.android.helper.mapToJsonElement import com.formbricks.android.logger.Logger -import com.formbricks.android.model.environment.EnvironmentDataHolder -import com.formbricks.android.model.environment.EnvironmentResponse +import com.formbricks.android.model.workspace.SegmentFilterResource +import com.formbricks.android.model.workspace.SegmentFilterResourceDeserializer +import com.formbricks.android.model.workspace.WorkspaceDataHolder +import com.formbricks.android.model.workspace.WorkspaceResponse import com.formbricks.android.model.user.PostUserBody import com.formbricks.android.model.user.UserResponse import com.google.gson.Gson -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.decodeFromJsonElement -import kotlinx.serialization.json.jsonObject +import com.google.gson.GsonBuilder import retrofit2.Call import retrofit2.Retrofit open class FormbricksApiService { private var retrofit: Retrofit? = null + private val gson: Gson = GsonBuilder() + .registerTypeAdapter( + SegmentFilterResource::class.java, + SegmentFilterResourceDeserializer() + ) + .create() + fun initialize(appUrl: String, isLoggingEnabled: Boolean) { val builder = FormbricksRetrofitBuilder(appUrl, isLoggingEnabled).getBuilder() if (builder != null) { @@ -29,29 +35,48 @@ open class FormbricksApiService { } } - open fun getEnvironmentStateObject(environmentId: String): Result { + open fun getWorkspaceStateObject(workspaceId: String): Result { return try { val retrofitInstance = retrofit ?: return Result.failure(RuntimeException("API service not initialized due to invalid URL")) val result = execute { retrofitInstance.create(FormbricksService::class.java) - .getEnvironmentState(environmentId) + .getWorkspaceState(workspaceId) } - val json = Json { ignoreUnknownKeys = true } val resultMap = result.getOrThrow() - val resultJson = mapToJsonElement(resultMap).jsonObject - val environmentResponse = json.decodeFromJsonElement(resultJson) - val data = EnvironmentDataHolder(environmentResponse.data, resultMap) + normalizeWorkspaceKeys(resultMap) + // Use Gson end-to-end so `@SerializedName(alternate=[...])` handles all + // the workspace-rename compatibility cases (settings/workspace/project, + // workspaceId/environmentId) and unknown server fields are ignored. + val resultJson = gson.toJson(resultMap) + val workspaceResponse = gson.fromJson(resultJson, WorkspaceResponse::class.java) + val data = WorkspaceDataHolder(workspaceResponse.data, resultMap) Result.success(data) } catch (e: Exception) { + Logger.e(RuntimeException("Failed to parse workspace state: ${e.message}", e)) Result.failure(e) } } - open fun postUser(environmentId: String, body: PostUserBody): Result { + /** + * Server may respond with `settings` (new), `workspace` (interim), or legacy + * `project` — plus sometimes more than one simultaneously. Pick one in order + * of preference and drop the rest so downstream decode only sees `settings`. + */ + @Suppress("UNCHECKED_CAST") + private fun normalizeWorkspaceKeys(resultMap: Map) { + val outer = resultMap["data"] as? MutableMap ?: return + val inner = outer["data"] as? MutableMap ?: return + val replacement = inner["settings"] ?: inner["workspace"] ?: inner["project"] ?: return + inner.remove("workspace") + inner.remove("project") + inner["settings"] = replacement + } + + open fun postUser(workspaceId: String, body: PostUserBody): Result { val retrofitInstance = retrofit ?: return Result.failure(RuntimeException("API service not initialized due to invalid URL")) return execute { retrofitInstance.create(FormbricksService::class.java) - .postUser(environmentId, body) + .postUser(workspaceId, body) } } @@ -75,4 +100,4 @@ open class FormbricksApiService { } } } -} \ No newline at end of file +} diff --git a/android/src/main/java/com/formbricks/android/network/FormbricksRetrofitBuilder.kt b/android/src/main/java/com/formbricks/android/network/FormbricksRetrofitBuilder.kt index 1e96314..b38a7a1 100644 --- a/android/src/main/java/com/formbricks/android/network/FormbricksRetrofitBuilder.kt +++ b/android/src/main/java/com/formbricks/android/network/FormbricksRetrofitBuilder.kt @@ -13,17 +13,17 @@ import java.util.concurrent.TimeUnit class FormbricksRetrofitBuilder(private val baseUrl: String, private val loggingEnabled: Boolean) { fun getBuilder(): Retrofit.Builder? { // Validate base URL is HTTPS - if (!baseUrl.startsWith("https://", ignoreCase = true)) { - val error = RuntimeException("Only HTTPS URLs are allowed. HTTP URLs are not permitted for security reasons. Provided URL: $baseUrl") - Logger.e(error) - return null - } +// if (!baseUrl.startsWith("https://", ignoreCase = true)) { +// val error = RuntimeException("Only HTTPS URLs are allowed. HTTP URLs are not permitted for security reasons. Provided URL: $baseUrl") +// Logger.e(error) +// return null +// } val clientBuilder = OkHttpClient.Builder() .connectTimeout(CONNECT_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS) .readTimeout(READ_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS) .followSslRedirects(true) - .addInterceptor(HttpsOnlyInterceptor()) +// .addInterceptor(HttpsOnlyInterceptor()) if (loggingEnabled) { val logging = HttpLoggingInterceptor() diff --git a/android/src/main/java/com/formbricks/android/network/FormbricksService.kt b/android/src/main/java/com/formbricks/android/network/FormbricksService.kt index 5ef320f..311e26f 100644 --- a/android/src/main/java/com/formbricks/android/network/FormbricksService.kt +++ b/android/src/main/java/com/formbricks/android/network/FormbricksService.kt @@ -9,13 +9,13 @@ import retrofit2.http.POST import retrofit2.http.Path interface FormbricksService { - @GET("$API_PREFIX/client/{environmentId}/environment") - fun getEnvironmentState(@Path("environmentId") environmentId: String): Call> + @GET("$API_PREFIX/client/{workspaceId}/environment") + fun getWorkspaceState(@Path("workspaceId") workspaceId: String): Call> - @POST("$API_PREFIX/client/{environmentId}/user") - fun postUser(@Path("environmentId") environmentId: String, @Body body: PostUserBody): Call + @POST("$API_PREFIX/client/{workspaceId}/user") + fun postUser(@Path("workspaceId") workspaceId: String, @Body body: PostUserBody): Call companion object { const val API_PREFIX = "/api/v2" } -} \ No newline at end of file +} diff --git a/android/src/main/java/com/formbricks/android/webview/FormbricksViewModel.kt b/android/src/main/java/com/formbricks/android/webview/FormbricksViewModel.kt index ba49b20..7c76e94 100644 --- a/android/src/main/java/com/formbricks/android/webview/FormbricksViewModel.kt +++ b/android/src/main/java/com/formbricks/android/webview/FormbricksViewModel.kt @@ -8,11 +8,11 @@ import com.formbricks.android.Formbricks import com.formbricks.android.extensions.guard import com.formbricks.android.manager.SurveyManager import com.formbricks.android.manager.UserManager -import com.formbricks.android.model.environment.EnvironmentDataHolder -import com.formbricks.android.model.environment.SurveyOverlay -import com.formbricks.android.model.environment.getProjectStylingJson -import com.formbricks.android.model.environment.getStyling -import com.formbricks.android.model.environment.getSurveyJson +import com.formbricks.android.model.workspace.WorkspaceDataHolder +import com.formbricks.android.model.workspace.SurveyOverlay +import com.formbricks.android.model.workspace.getSettingsStylingJson +import com.formbricks.android.model.workspace.getStyling +import com.formbricks.android.model.workspace.getSurveyJson import com.google.gson.JsonObject /** @@ -29,7 +29,7 @@ class FormbricksViewModel : ViewModel() { - + Formbricks WebView Survey @@ -44,7 +44,7 @@ class FormbricksViewModel : ViewModel() { function onClose() { FormbricksJavascript.message(JSON.stringify({ event: "onClose" })); }; - + function onDisplayCreated() { FormbricksJavascript.message(JSON.stringify({ event: "onDisplayCreated" })); }; @@ -52,12 +52,12 @@ class FormbricksViewModel : ViewModel() { function onResponseCreated() { FormbricksJavascript.message(JSON.stringify({ event: "onResponseCreated" })); }; - + let setResponseFinished = null; function getSetIsResponseSendingFinished(callback) { setResponseFinished = callback; - } - + } + function loadSurvey() { const options = JSON.parse(json); const surveyProps = { @@ -76,10 +76,10 @@ class FormbricksViewModel : ViewModel() { inputs.forEach(input => { if (!input.getAttribute('data-file-picker-overridden')) { input.setAttribute('data-file-picker-overridden', 'true'); - + const allowedFileExtensions = input.getAttribute('data-accept-extensions'); const allowMultipleFiles = input.getAttribute('data-accept-multiple'); - + input.addEventListener('click', function (e) { e.preventDefault(); FormbricksJavascript.message(JSON.stringify({ @@ -93,13 +93,13 @@ class FormbricksViewModel : ViewModel() { } }); }; - + attachFilePickerOverride(); - + const observer = new MutationObserver(function (mutations) { attachFilePickerOverride(); }); - + observer.observe(document.body, { childList: true, subtree: true }); const script = document.createElement("script"); script.src = "${Formbricks.appUrl}/js/surveys.umd.cjs"; @@ -115,23 +115,26 @@ class FormbricksViewModel : ViewModel() { """ fun loadHtml(surveyId: String) { - val environment = SurveyManager.environmentDataHolder.guard { return } - val json = getJson(environment, surveyId) + val workspace = SurveyManager.workspaceDataHolder.guard { return } + val json = getJson(workspace, surveyId) val htmlString = htmlTemplate.replace("{{WEBVIEW_DATA}}", json) html.postValue(htmlString) } - private fun getJson(environmentDataHolder: EnvironmentDataHolder, surveyId: String): String { + private fun getJson(workspaceDataHolder: WorkspaceDataHolder, surveyId: String): String { val jsonObject = JsonObject() - environmentDataHolder.getSurveyJson(surveyId).let { jsonObject.add("survey", it) } - jsonObject.addProperty("isBrandingEnabled", environmentDataHolder.data?.data?.project?.inAppSurveyBranding ?: true) + workspaceDataHolder.getSurveyJson(surveyId).let { jsonObject.add("survey", it) } + jsonObject.addProperty("isBrandingEnabled", workspaceDataHolder.data?.data?.settings?.inAppSurveyBranding ?: true) jsonObject.addProperty("appUrl", Formbricks.appUrl) - jsonObject.addProperty("environmentId", Formbricks.environmentId) + jsonObject.addProperty("workspaceId", Formbricks.workspaceId) + // Keep `environmentId` in the payload for backward compatibility with older + // survey-script versions that still read it. + jsonObject.addProperty("environmentId", Formbricks.workspaceId) jsonObject.addProperty("contactId", UserManager.contactId) jsonObject.addProperty("isWebEnvironment", false) - val matchedSurvey = environmentDataHolder.data?.data?.surveys?.first { it.id == surveyId } - val project = environmentDataHolder.data?.data?.project + val matchedSurvey = workspaceDataHolder.data?.data?.surveys?.first { it.id == surveyId } + val settings = workspaceDataHolder.data?.data?.settings val isMultiLangSurvey = (matchedSurvey?.languages?.size @@ -145,20 +148,20 @@ class FormbricksViewModel : ViewModel() { val hasCustomStyling = matchedSurvey?.styling != null - val placement = matchedSurvey?.projectOverwrites?.placement ?: project?.placement + val placement = matchedSurvey?.projectOverwrites?.placement ?: settings?.placement if (placement != null) jsonObject.addProperty("placement", placement) - val clickOutside = matchedSurvey?.projectOverwrites?.clickOutsideClose ?: project?.clickOutsideClose ?: false + val clickOutside = matchedSurvey?.projectOverwrites?.clickOutsideClose ?: settings?.clickOutsideClose ?: false jsonObject.addProperty("clickOutside", clickOutside) - val overlay = (matchedSurvey?.projectOverwrites?.overlay ?: project?.overlay ?: SurveyOverlay.NONE).value + val overlay = (matchedSurvey?.projectOverwrites?.overlay ?: settings?.overlay ?: SurveyOverlay.NONE).value jsonObject.addProperty("overlay", overlay) - val enabled = project?.styling?.allowStyleOverwrite ?: false + val enabled = settings?.styling?.allowStyleOverwrite ?: false if (hasCustomStyling && enabled) { - environmentDataHolder.getStyling(surveyId)?.let { jsonObject.add("styling", it) } + workspaceDataHolder.getStyling(surveyId)?.let { jsonObject.add("styling", it) } } else { - environmentDataHolder.getProjectStylingJson()?.let { jsonObject.add("styling", it) } + workspaceDataHolder.getSettingsStylingJson()?.let { jsonObject.add("styling", it) } } return jsonObject.toString() @@ -170,4 +173,4 @@ class FormbricksViewModel : ViewModel() { @BindingAdapter("htmlText") fun WebView.setHtmlText(htmlString: String?) { loadData(htmlString ?: "", "text/html", "UTF-8") -} \ No newline at end of file +} From 6c9200b3af3399ee9c58cfe2de19999b6e7ce25e Mon Sep 17 00:00:00 2001 From: pandeymangg Date: Thu, 23 Apr 2026 18:40:58 +0530 Subject: [PATCH 2/4] chore: restore HTTPS validation and revert test version bump Undo local-publish hacks from the workspace-support feature commit: re-enable the HTTPS guard in Formbricks.setup and revert the version back to 1.2.0 so a release can be cut cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) --- android/build.gradle.kts | 2 +- .../src/main/java/com/formbricks/android/Formbricks.kt | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/android/build.gradle.kts b/android/build.gradle.kts index e7d46ca..02c6d7b 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -11,7 +11,7 @@ plugins { id("org.sonarqube") version "4.4.1.3373" } -version = "1.3.1" +version = "1.2.0" val groupId = "com.formbricks" val artifactId = "android" diff --git a/android/src/main/java/com/formbricks/android/Formbricks.kt b/android/src/main/java/com/formbricks/android/Formbricks.kt index 544b9b1..a02d4b9 100644 --- a/android/src/main/java/com/formbricks/android/Formbricks.kt +++ b/android/src/main/java/com/formbricks/android/Formbricks.kt @@ -70,11 +70,11 @@ object Formbricks { // 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}") -// Logger.e(error) -// return -// } + 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}") + Logger.e(error) + return + } applicationContext = context From 56ea0c1a77105850a7f03069d641c9f780cd765d Mon Sep 17 00:00:00 2001 From: pandeymangg Date: Fri, 24 Apr 2026 16:23:32 +0530 Subject: [PATCH 3/4] fixes coderabbit feedback --- android/build.gradle.kts | 8 ++--- .../{Environment.json => Workspace.json} | 0 .../android/MockFormbricksApiService.kt | 2 +- .../WorkspaceDataMigrationInstrumentedTest.kt | 12 ++++--- .../java/com/formbricks/android/Formbricks.kt | 3 +- .../android/manager/SurveyManager.kt | 33 +++++++++---------- .../android/network/FormbricksApiService.kt | 25 ++++++++------ .../android/webview/FormbricksViewModel.kt | 12 +++++-- 8 files changed, 54 insertions(+), 41 deletions(-) rename android/src/androidTest/assets/{Environment.json => Workspace.json} (100%) diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 02c6d7b..e8a6e54 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -95,7 +95,7 @@ dependencies { mavenPublishing { publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) - // signAllPublications() // disabled for local publishing + signAllPublications() // disabled for local publishing coordinates(groupId, artifactId, version.toString()) @@ -127,7 +127,7 @@ mavenPublishing { // Add JaCoCo tasks tasks.register("jacocoAndroidTestReport") { dependsOn("connectedDebugAndroidTest") - + reports { xml.required.set(true) html.required.set(true) @@ -174,11 +174,11 @@ tasks.register("jacocoAndroidTestReport") { // Configure Sonar sonar { properties { - property("sonar.coverage.jacoco.xmlReportPaths", + property("sonar.coverage.jacoco.xmlReportPaths", layout.buildDirectory.file("reports/jacoco/jacocoAndroidTestReport/jacocoAndroidTestReport.xml").get().asFile.path) } } tasks.sonar { dependsOn("jacocoAndroidTestReport") -} \ No newline at end of file +} diff --git a/android/src/androidTest/assets/Environment.json b/android/src/androidTest/assets/Workspace.json similarity index 100% rename from android/src/androidTest/assets/Environment.json rename to android/src/androidTest/assets/Workspace.json diff --git a/android/src/androidTest/java/com/formbricks/android/MockFormbricksApiService.kt b/android/src/androidTest/java/com/formbricks/android/MockFormbricksApiService.kt index c8f1a5d..60a80f1 100644 --- a/android/src/androidTest/java/com/formbricks/android/MockFormbricksApiService.kt +++ b/android/src/androidTest/java/com/formbricks/android/MockFormbricksApiService.kt @@ -17,7 +17,7 @@ class MockFormbricksApiService: FormbricksApiService() { init { val context = InstrumentationRegistry.getInstrumentation().context - val workspaceJson = context.assets.open("Environment.json").bufferedReader().readText() + val workspaceJson = context.assets.open("Workspace.json").bufferedReader().readText() val userJson = context.assets.open("User.json").bufferedReader().readText() workspace = gson.fromJson(workspaceJson, WorkspaceResponse::class.java) diff --git a/android/src/androidTest/java/com/formbricks/android/manager/WorkspaceDataMigrationInstrumentedTest.kt b/android/src/androidTest/java/com/formbricks/android/manager/WorkspaceDataMigrationInstrumentedTest.kt index 28fd56d..0d607dc 100644 --- a/android/src/androidTest/java/com/formbricks/android/manager/WorkspaceDataMigrationInstrumentedTest.kt +++ b/android/src/androidTest/java/com/formbricks/android/manager/WorkspaceDataMigrationInstrumentedTest.kt @@ -125,11 +125,12 @@ class WorkspaceDataMigrationInstrumentedTest { } /** - * A cache blob written under the pre-rename SharedPreferences key should be read - * once, migrated to the new key, and then removed from the legacy slot. + * A cache blob written under the pre-rename SharedPreferences key should be + * copied to the new key and dropped from the legacy slot when + * `migrateLegacyCacheIfNeeded` runs at setup time. */ @Test - fun testLegacyCachedDataHolderIsMigratedOnRead() { + fun testLegacyCachedDataHolderIsMigratedOnSetup() { val legacyBlob = """ { "data": { @@ -156,9 +157,10 @@ class WorkspaceDataMigrationInstrumentedTest { .putString(SurveyManager.PREF_LEGACY_ENVIRONMENT_DATA_HOLDER, legacyBlob) .apply() - // Reading should migrate and return a decoded WorkspaceDataHolder. + SurveyManager.migrateLegacyCacheIfNeeded() + val holder = SurveyManager.workspaceDataHolder - assertNotNull("Legacy cache should be read and decoded", holder) + assertNotNull("Migrated cache should be read and decoded", holder) assertEquals("bottomRight", holder?.data?.data?.settings?.placement) // Legacy key is gone, new key is populated. diff --git a/android/src/main/java/com/formbricks/android/Formbricks.kt b/android/src/main/java/com/formbricks/android/Formbricks.kt index a02d4b9..bac728e 100644 --- a/android/src/main/java/com/formbricks/android/Formbricks.kt +++ b/android/src/main/java/com/formbricks/android/Formbricks.kt @@ -85,7 +85,7 @@ object Formbricks { fragmentManager = config.fragmentManager if (config.usedDeprecatedEnvironmentId) { - Logger.d("environmentId is deprecated and will be removed in a future version. Please use workspaceId instead.") + Logger.w("environmentId is deprecated and will be removed in a future version. Please use workspaceId instead.") } config.userId?.let { UserManager.set(it) } @@ -96,6 +96,7 @@ object Formbricks { } FormbricksApi.initialize() + SurveyManager.migrateLegacyCacheIfNeeded() SurveyManager.refreshWorkspaceIfNeeded(force = forceRefresh) UserManager.syncUserStateIfNeeded() 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 e8a81cd..b9cb9bf 100644 --- a/android/src/main/java/com/formbricks/android/manager/SurveyManager.kt +++ b/android/src/main/java/com/formbricks/android/manager/SurveyManager.kt @@ -49,23 +49,7 @@ object SurveyManager { .create() private var workspaceDataHolderJson: String? - get() { - // Prefer the new key; fall back to the legacy key for installs that still - // have data stored under the pre-rename `formbricksDataHolder`. - val current = prefManager.getString(PREF_FORMBRICKS_WORKSPACE_DATA_HOLDER, null) - if (current != null) return current - - val legacy = prefManager.getString(PREF_LEGACY_ENVIRONMENT_DATA_HOLDER, null) - if (legacy != null) { - // Migrate the legacy blob to the new key and drop the old one. - prefManager.edit() - .putString(PREF_FORMBRICKS_WORKSPACE_DATA_HOLDER, legacy) - .remove(PREF_LEGACY_ENVIRONMENT_DATA_HOLDER) - .apply() - return legacy - } - return null - } + get() = prefManager.getString(PREF_FORMBRICKS_WORKSPACE_DATA_HOLDER, null) set(value) { val editor = prefManager.edit() if (null != value) { @@ -78,6 +62,21 @@ object SurveyManager { editor.apply() } + /** + * One-shot migration of the pre-rename SharedPreferences cache. Call once during + * SDK setup before any reads. If a legacy blob exists and the new key is empty, + * copy it over; always drop the legacy key afterwards. + */ + internal fun migrateLegacyCacheIfNeeded() { + if (!prefManager.contains(PREF_LEGACY_ENVIRONMENT_DATA_HOLDER)) return + val legacy = prefManager.getString(PREF_LEGACY_ENVIRONMENT_DATA_HOLDER, null) + val editor = prefManager.edit().remove(PREF_LEGACY_ENVIRONMENT_DATA_HOLDER) + if (legacy != null && !prefManager.contains(PREF_FORMBRICKS_WORKSPACE_DATA_HOLDER)) { + editor.putString(PREF_FORMBRICKS_WORKSPACE_DATA_HOLDER, legacy) + } + editor.apply() + } + private var backingWorkspaceDataHolder: WorkspaceDataHolder? = null var workspaceDataHolder: WorkspaceDataHolder? get() { diff --git a/android/src/main/java/com/formbricks/android/network/FormbricksApiService.kt b/android/src/main/java/com/formbricks/android/network/FormbricksApiService.kt index 3a47b78..87d6534 100644 --- a/android/src/main/java/com/formbricks/android/network/FormbricksApiService.kt +++ b/android/src/main/java/com/formbricks/android/network/FormbricksApiService.kt @@ -43,13 +43,13 @@ open class FormbricksApiService { .getWorkspaceState(workspaceId) } val resultMap = result.getOrThrow() - normalizeWorkspaceKeys(resultMap) + val normalizedMap = normalizeWorkspaceKeys(resultMap) // Use Gson end-to-end so `@SerializedName(alternate=[...])` handles all // the workspace-rename compatibility cases (settings/workspace/project, // workspaceId/environmentId) and unknown server fields are ignored. - val resultJson = gson.toJson(resultMap) + val resultJson = gson.toJson(normalizedMap) val workspaceResponse = gson.fromJson(resultJson, WorkspaceResponse::class.java) - val data = WorkspaceDataHolder(workspaceResponse.data, resultMap) + val data = WorkspaceDataHolder(workspaceResponse.data, normalizedMap) Result.success(data) } catch (e: Exception) { Logger.e(RuntimeException("Failed to parse workspace state: ${e.message}", e)) @@ -61,15 +61,20 @@ open class FormbricksApiService { * Server may respond with `settings` (new), `workspace` (interim), or legacy * `project` — plus sometimes more than one simultaneously. Pick one in order * of preference and drop the rest so downstream decode only sees `settings`. + * Returns a shallow copy; the input map is not mutated. */ @Suppress("UNCHECKED_CAST") - private fun normalizeWorkspaceKeys(resultMap: Map) { - val outer = resultMap["data"] as? MutableMap ?: return - val inner = outer["data"] as? MutableMap ?: return - val replacement = inner["settings"] ?: inner["workspace"] ?: inner["project"] ?: return - inner.remove("workspace") - inner.remove("project") - inner["settings"] = replacement + private fun normalizeWorkspaceKeys(resultMap: Map): Map { + val outer = resultMap["data"] as? Map ?: return resultMap + val inner = outer["data"] as? Map ?: return resultMap + val replacement = inner["settings"] ?: inner["workspace"] ?: inner["project"] ?: return resultMap + val newInner = inner.toMutableMap().apply { + remove("workspace") + remove("project") + put("settings", replacement) + } + val newOuter = outer.toMutableMap().apply { put("data", newInner) } + return resultMap.toMutableMap().apply { put("data", newOuter) } } open fun postUser(workspaceId: String, body: PostUserBody): Result { diff --git a/android/src/main/java/com/formbricks/android/webview/FormbricksViewModel.kt b/android/src/main/java/com/formbricks/android/webview/FormbricksViewModel.kt index 7c76e94..b222f86 100644 --- a/android/src/main/java/com/formbricks/android/webview/FormbricksViewModel.kt +++ b/android/src/main/java/com/formbricks/android/webview/FormbricksViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.formbricks.android.Formbricks import com.formbricks.android.extensions.guard +import com.formbricks.android.logger.Logger import com.formbricks.android.manager.SurveyManager import com.formbricks.android.manager.UserManager import com.formbricks.android.model.workspace.WorkspaceDataHolder @@ -116,12 +117,18 @@ class FormbricksViewModel : ViewModel() { fun loadHtml(surveyId: String) { val workspace = SurveyManager.workspaceDataHolder.guard { return } - val json = getJson(workspace, surveyId) + val json = getJson(workspace, surveyId) ?: return val htmlString = htmlTemplate.replace("{{WEBVIEW_DATA}}", json) html.postValue(htmlString) } - private fun getJson(workspaceDataHolder: WorkspaceDataHolder, surveyId: String): String { + private fun getJson(workspaceDataHolder: WorkspaceDataHolder, surveyId: String): String? { + val matchedSurvey = workspaceDataHolder.data?.data?.surveys?.firstOrNull { it.id == surveyId } + if (matchedSurvey == null) { + Logger.w("Survey with id $surveyId not found in workspace data; skipping render") + return null + } + val jsonObject = JsonObject() workspaceDataHolder.getSurveyJson(surveyId).let { jsonObject.add("survey", it) } jsonObject.addProperty("isBrandingEnabled", workspaceDataHolder.data?.data?.settings?.inAppSurveyBranding ?: true) @@ -133,7 +140,6 @@ class FormbricksViewModel : ViewModel() { jsonObject.addProperty("contactId", UserManager.contactId) jsonObject.addProperty("isWebEnvironment", false) - val matchedSurvey = workspaceDataHolder.data?.data?.surveys?.first { it.id == surveyId } val settings = workspaceDataHolder.data?.data?.settings val isMultiLangSurvey = From 74ad002bc0ae88be41759590d8adb7d8c8f62a72 Mon Sep 17 00:00:00 2001 From: pandeymangg Date: Fri, 24 Apr 2026 16:36:31 +0530 Subject: [PATCH 4/4] fixes tests --- .../android/webview/FormbricksViewModel.kt | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/android/src/main/java/com/formbricks/android/webview/FormbricksViewModel.kt b/android/src/main/java/com/formbricks/android/webview/FormbricksViewModel.kt index b222f86..2f113e5 100644 --- a/android/src/main/java/com/formbricks/android/webview/FormbricksViewModel.kt +++ b/android/src/main/java/com/formbricks/android/webview/FormbricksViewModel.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.formbricks.android.Formbricks import com.formbricks.android.extensions.guard -import com.formbricks.android.logger.Logger import com.formbricks.android.manager.SurveyManager import com.formbricks.android.manager.UserManager import com.formbricks.android.model.workspace.WorkspaceDataHolder @@ -117,18 +116,12 @@ class FormbricksViewModel : ViewModel() { fun loadHtml(surveyId: String) { val workspace = SurveyManager.workspaceDataHolder.guard { return } - val json = getJson(workspace, surveyId) ?: return + val json = getJson(workspace, surveyId) val htmlString = htmlTemplate.replace("{{WEBVIEW_DATA}}", json) html.postValue(htmlString) } - private fun getJson(workspaceDataHolder: WorkspaceDataHolder, surveyId: String): String? { - val matchedSurvey = workspaceDataHolder.data?.data?.surveys?.firstOrNull { it.id == surveyId } - if (matchedSurvey == null) { - Logger.w("Survey with id $surveyId not found in workspace data; skipping render") - return null - } - + private fun getJson(workspaceDataHolder: WorkspaceDataHolder, surveyId: String): String { val jsonObject = JsonObject() workspaceDataHolder.getSurveyJson(surveyId).let { jsonObject.add("survey", it) } jsonObject.addProperty("isBrandingEnabled", workspaceDataHolder.data?.data?.settings?.inAppSurveyBranding ?: true) @@ -140,6 +133,7 @@ class FormbricksViewModel : ViewModel() { jsonObject.addProperty("contactId", UserManager.contactId) jsonObject.addProperty("isWebEnvironment", false) + val matchedSurvey = workspaceDataHolder.data?.data?.surveys?.firstOrNull { it.id == surveyId } val settings = workspaceDataHolder.data?.data?.settings val isMultiLangSurvey =