diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 22f93e0530..9d754f3add 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -283,6 +283,8 @@ dependencies {
implementation(projects.features.onboardingV2.impl)
implementation(projects.features.stories.api)
implementation(projects.features.stories.impl)
+ implementation(projects.features.survey.api)
+ implementation(projects.features.survey.impl)
implementation(projects.features.txhistory.api)
implementation(projects.features.txhistory.impl)
implementation(projects.features.biometry.api)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f6139be58a..36cca67394 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -210,6 +210,17 @@
android:scheme="tangem" />
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/tangem/tap/common/deeplink/DefaultDeeplinkLauncher.kt b/app/src/main/java/com/tangem/tap/common/deeplink/DefaultDeeplinkLauncher.kt
index 0b6f9c503e..61ac194a7f 100644
--- a/app/src/main/java/com/tangem/tap/common/deeplink/DefaultDeeplinkLauncher.kt
+++ b/app/src/main/java/com/tangem/tap/common/deeplink/DefaultDeeplinkLauncher.kt
@@ -6,6 +6,8 @@ import android.net.Uri
import androidx.core.net.toUri
import com.tangem.common.routing.DeepLinkScheme
import com.tangem.common.uri.ExternalUrlValidator
+import com.tangem.core.analytics.api.AnalyticsExceptionHandler
+import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.navigation.deeplink.DeeplinkLauncher
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.utils.logging.TangemLogger
@@ -17,6 +19,7 @@ import com.tangem.utils.logging.TangemLogger
internal class DefaultDeeplinkLauncher(
private val context: Context,
private val urlOpener: UrlOpener,
+ private val analyticsExceptionHandler: AnalyticsExceptionHandler,
) : DeeplinkLauncher {
override fun launch(link: String) {
@@ -58,11 +61,33 @@ internal class DefaultDeeplinkLauncher(
}
private fun launchDeepLink(uri: Uri) {
- context.startActivity(createDeepLinkIntent(uri))
+ val intent = createDeepLinkIntent(uri)
+ if (intent.resolveActivity(context.packageManager) != null) {
+ context.startActivity(intent)
+ } else {
+ TangemLogger.i(
+ """
+ No match found for deep link
+ |- Received URI: $uri
+ """.trimIndent(),
+ )
+ analyticsExceptionHandler.sendException(
+ ExceptionAnalyticsEvent(
+ exception = UnresolvedDeeplinkException(uri),
+ params = mapOf(
+ "uri_scheme" to uri.scheme.orEmpty(),
+ "uri_host" to uri.host.orEmpty(),
+ ),
+ ),
+ )
+ }
}
private fun createDeepLinkIntent(uri: Uri): Intent = Intent(Intent.ACTION_VIEW, uri).apply {
setPackage(context.packageName)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
-}
\ No newline at end of file
+}
+
+internal class UnresolvedDeeplinkException(uri: Uri) :
+ RuntimeException("Deeplink has no matching activity: scheme=${uri.scheme}, host=${uri.host}")
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt
index 492904d18a..d8cb34aece 100644
--- a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt
@@ -1,6 +1,7 @@
package com.tangem.tap.di
import android.content.Context
+import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.tap.common.deeplink.DefaultDeeplinkLauncher
import com.tangem.core.navigation.deeplink.DeeplinkLauncher
import com.tangem.core.navigation.finisher.AppFinisher
@@ -55,7 +56,10 @@ internal interface UtilsModule {
@Provides
@Singleton
- fun provideDeeplinkLauncher(@ApplicationContext context: Context, urlOpener: UrlOpener): DeeplinkLauncher =
- DefaultDeeplinkLauncher(context, urlOpener)
+ fun provideDeeplinkLauncher(
+ @ApplicationContext context: Context,
+ urlOpener: UrlOpener,
+ analyticsExceptionHandler: AnalyticsExceptionHandler,
+ ): DeeplinkLauncher = DefaultDeeplinkLauncher(context, urlOpener, analyticsExceptionHandler)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt
index c1d690f533..41205cfc6e 100644
--- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt
+++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt
@@ -21,6 +21,7 @@ import com.tangem.features.feed.entry.components.FeedEntryRoute
import com.tangem.features.home.api.HomeComponent
import com.tangem.features.hotwallet.*
import com.tangem.features.kyc.KycComponent
+import com.tangem.features.survey.SurveyComponent
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensMode
@@ -112,6 +113,7 @@ internal class ChildFactory @Inject constructor(
private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory,
private val tangemPayWalletOnboardingComponentFactory: TangemPayHotWalletOnboardingComponent.Factory,
private val kycComponentFactory: KycComponent.Factory,
+ private val surveyComponentFactory: SurveyComponent.Factory,
private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
private val feedEntryComponentFactory: FeedEntryComponent.Factory,
private val addFundsComponentFactory: AddFundsComponent.Factory,
@@ -702,6 +704,13 @@ internal class ChildFactory @Inject constructor(
componentFactory = kycComponentFactory,
)
}
+ is AppRoute.Survey -> {
+ createComponentChild(
+ context = context,
+ params = SurveyComponent.Params(token = route.token, displayId = route.displayId),
+ componentFactory = surveyComponentFactory,
+ )
+ }
is AppRoute.YieldSupplyEntry -> {
createComponentChild(
context = context,
diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt
index 72fdba54ab..e886e10e14 100644
--- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt
+++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt
@@ -19,6 +19,7 @@ import com.tangem.features.onramp.deeplink.SellDeepLinkHandler
import com.tangem.features.onramp.deeplink.SwapDeepLinkHandler
import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler
import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler
+import com.tangem.features.survey.deeplink.SurveyDeepLinkHandler
import com.tangem.features.tangempay.deeplink.OnboardVisaDeepLinkHandler
import com.tangem.features.tangempay.deeplink.TangemPayMainDeepLinkHandler
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
@@ -62,6 +63,7 @@ internal class DeepLinkFactory @Inject constructor(
private val newsDeepLink: NewsDeepLinkHandler.Factory,
private val earnDeepLink: EarnDeepLinkHandler.Factory,
private val yieldDeepLink: YieldDeepLinkHandler.Factory,
+ private val surveyDeepLink: SurveyDeepLinkHandler.Factory,
) {
private val permittedAppRoute = MutableStateFlow(false)
@@ -175,6 +177,7 @@ internal class DeepLinkFactory @Inject constructor(
DeepLinkRoute.Earn.host -> earnDeepLink.create(queryParams)
DeepLinkRoute.Yield.host -> yieldDeepLink.create(coroutineScope, queryParams)
DeepLinkRoute.PayAppMain.host -> tangemPayMainDeepLink.create(coroutineScope, queryParams)
+ DeepLinkRoute.Survey.host -> surveyDeepLink.create(queryParams)
else -> {
TangemLogger.i(
"""
diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt
index b256302118..1650ffedea 100644
--- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt
+++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt
@@ -11,6 +11,7 @@ import com.tangem.features.feed.entry.deeplink.MarketsTokenExchangesDeepLinkHand
import com.tangem.features.feed.entry.deeplink.NewsDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.YieldDeepLinkHandler
+import com.tangem.features.survey.deeplink.SurveyDeepLinkHandler
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
import com.tangem.features.onramp.deeplink.SellDeepLinkHandler
@@ -99,6 +100,10 @@ class DeepLinkFactoryTest {
every { create(any()) } returns mockk()
}
+ private val surveyDeepLinkFactory = mockk(relaxed = true) {
+ every { create(any()) } returns mockk()
+ }
+
private val earnDeepLinkFactory = mockk(relaxed = true) {
every { create(any()) } returns mockk()
}
@@ -140,6 +145,7 @@ class DeepLinkFactoryTest {
newsDeepLink = newsDeepLinkFactory,
earnDeepLink = earnDeepLinkFactory,
yieldDeepLink = yieldDeepLinkFactory,
+ surveyDeepLink = surveyDeepLinkFactory,
)
@OptIn(ExperimentalCoroutinesApi::class)
diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt
index 30438d4e34..02d03d772d 100644
--- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt
+++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt
@@ -498,6 +498,9 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class Kyc(val userWalletId: UserWalletId) : AppRoute(path = "/kyc")
+ @Serializable
+ data class Survey(val token: String, val displayId: String? = null) : AppRoute(path = "/survey")
+
@Serializable
data class YieldSupplyEntry(
val userWalletId: UserWalletId,
diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt
index ee03ee94b4..e2e31a626c 100644
--- a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt
+++ b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt
@@ -87,6 +87,10 @@ sealed class DeepLinkRoute {
data object PayAppMain : DeepLinkRoute() {
override val host: String = "pay-app-main"
}
+
+ data object Survey : DeepLinkRoute() {
+ override val host: String = "survey"
+ }
}
enum class DeepLinkScheme(val scheme: String) {
diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json
index d465fd6778..2fe3cc2043 100644
--- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json
+++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json
@@ -115,6 +115,10 @@
"name": "AND_15438_BACKEND_AUTHENTICATION_ENABLED",
"version": "undefined"
},
+ {
+ "name": "AND_15482_SURVEYSPARROW_ENABLED",
+ "version": "undefined"
+ },
{
"name": "AND_15258_QUICK_TOP_UP_ENABLED",
"version": "undefined"
diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt
index 7abc5a48bd..108b6fe3e5 100644
--- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt
+++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt
@@ -1,5 +1,6 @@
package com.tangem.features.promobanners.impl.model
+import androidx.core.net.toUri
import com.tangem.core.navigation.deeplink.DeeplinkLauncher
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
@@ -118,7 +119,18 @@ internal class PromoBannersBlockModel @Inject constructor(
private fun onButtonClick(displayId: Int, deeplink: String?) {
analyticsEventHandler.send(PromoBannerAnalyticsEvent.Clicked(displayId, placeholderName))
- deeplink?.let { deeplinkLauncher.launch(it) }
+ deeplink?.let { deeplinkLauncher.launch(appendSurveyDisplayId(it, displayId)) }
+ }
+
+ private fun appendSurveyDisplayId(deeplink: String, displayId: Int): String {
+ val uri = deeplink.toUri()
+ val isSurveyDeeplink = uri.scheme == DEEPLINK_SCHEME_TANGEM && uri.host == DEEPLINK_HOST_SURVEY
+ if (!isSurveyDeeplink || uri.getQueryParameter(QUERY_DISPLAY_ID) != null) return deeplink
+
+ return uri.buildUpon()
+ .appendQueryParameter(QUERY_DISPLAY_ID, displayId.toString())
+ .build()
+ .toString()
}
private fun getInitialState() = PromoBannersBlockUM(
@@ -152,4 +164,10 @@ internal class PromoBannersBlockModel @Inject constructor(
}
}
}
+
+ private companion object {
+ const val DEEPLINK_SCHEME_TANGEM = "tangem"
+ const val DEEPLINK_HOST_SURVEY = "survey"
+ const val QUERY_DISPLAY_ID = "display_id"
+ }
}
\ No newline at end of file
diff --git a/features/survey/api/build.gradle.kts b/features/survey/api/build.gradle.kts
new file mode 100644
index 0000000000..a0db7dc04e
--- /dev/null
+++ b/features/survey/api/build.gradle.kts
@@ -0,0 +1,14 @@
+plugins {
+ alias(deps.plugins.android.library)
+ alias(deps.plugins.kotlin.android)
+ id("configuration")
+}
+
+android {
+ namespace = "com.tangem.features.survey.api"
+}
+
+dependencies {
+ implementation(projects.core.decompose)
+ implementation(projects.core.ui)
+}
\ No newline at end of file
diff --git a/features/survey/api/src/main/kotlin/com/tangem/features/survey/SurveyComponent.kt b/features/survey/api/src/main/kotlin/com/tangem/features/survey/SurveyComponent.kt
new file mode 100644
index 0000000000..4fe9360d56
--- /dev/null
+++ b/features/survey/api/src/main/kotlin/com/tangem/features/survey/SurveyComponent.kt
@@ -0,0 +1,11 @@
+package com.tangem.features.survey
+
+import com.tangem.core.decompose.factory.ComponentFactory
+import com.tangem.core.ui.decompose.ComposableContentComponent
+
+interface SurveyComponent : ComposableContentComponent {
+
+ data class Params(val token: String, val displayId: String?)
+
+ interface Factory : ComponentFactory
+}
\ No newline at end of file
diff --git a/features/survey/api/src/main/kotlin/com/tangem/features/survey/SurveyFeatureToggles.kt b/features/survey/api/src/main/kotlin/com/tangem/features/survey/SurveyFeatureToggles.kt
new file mode 100644
index 0000000000..c42ffbf905
--- /dev/null
+++ b/features/survey/api/src/main/kotlin/com/tangem/features/survey/SurveyFeatureToggles.kt
@@ -0,0 +1,6 @@
+package com.tangem.features.survey
+
+interface SurveyFeatureToggles {
+
+ val areSurveysEnabled: Boolean
+}
\ No newline at end of file
diff --git a/features/survey/api/src/main/kotlin/com/tangem/features/survey/SurveySparrowLauncher.kt b/features/survey/api/src/main/kotlin/com/tangem/features/survey/SurveySparrowLauncher.kt
new file mode 100644
index 0000000000..84d518d39e
--- /dev/null
+++ b/features/survey/api/src/main/kotlin/com/tangem/features/survey/SurveySparrowLauncher.kt
@@ -0,0 +1,14 @@
+package com.tangem.features.survey
+
+import android.app.Activity
+
+interface SurveySparrowLauncher {
+
+ fun present(activity: Activity, data: SurveyLaunchData)
+}
+
+data class SurveyLaunchData(
+ val domain: String,
+ val token: String,
+ val customParams: Map,
+)
\ No newline at end of file
diff --git a/features/survey/api/src/main/kotlin/com/tangem/features/survey/deeplink/SurveyDeepLinkHandler.kt b/features/survey/api/src/main/kotlin/com/tangem/features/survey/deeplink/SurveyDeepLinkHandler.kt
new file mode 100644
index 0000000000..a1d6abc36d
--- /dev/null
+++ b/features/survey/api/src/main/kotlin/com/tangem/features/survey/deeplink/SurveyDeepLinkHandler.kt
@@ -0,0 +1,8 @@
+package com.tangem.features.survey.deeplink
+
+interface SurveyDeepLinkHandler {
+
+ interface Factory {
+ fun create(queryParams: Map): SurveyDeepLinkHandler
+ }
+}
\ No newline at end of file
diff --git a/features/survey/impl/build.gradle.kts b/features/survey/impl/build.gradle.kts
new file mode 100644
index 0000000000..e512a05413
--- /dev/null
+++ b/features/survey/impl/build.gradle.kts
@@ -0,0 +1,61 @@
+plugins {
+ alias(deps.plugins.android.library)
+ alias(deps.plugins.kotlin.android)
+ alias(deps.plugins.kotlin.kapt)
+ alias(deps.plugins.hilt.android)
+ id("configuration")
+}
+
+android {
+ namespace = "com.tangem.features.survey.impl"
+}
+
+tasks.withType().configureEach {
+ useJUnitPlatform()
+}
+
+dependencies {
+ /* Project - API */
+ implementation(projects.features.survey.api)
+
+ /* Domain */
+ implementation(projects.domain.common)
+ implementation(projects.domain.models)
+ implementation(projects.domain.wallets)
+ implementation(projects.domain.wallets.models)
+
+ /* Core */
+ implementation(projects.core.analytics)
+ implementation(projects.core.analytics.models)
+ implementation(projects.core.configToggles)
+ implementation(projects.core.datasource)
+ implementation(projects.core.decompose)
+ implementation(projects.core.ui)
+ implementation(projects.core.utils)
+
+ /* Common */
+ implementation(projects.common.routing)
+
+ /* DI */
+ implementation(deps.hilt.android)
+ kapt(deps.hilt.kapt)
+
+ /* Compose */
+ implementation(deps.compose.runtime)
+ implementation(deps.compose.ui)
+
+ /* Other */
+ implementation(deps.kotlin.coroutines)
+ implementation(deps.arrow.core)
+
+ /** Tangem libraries */
+ implementation(tangemDeps.card.core)
+ implementation(deps.surveysparrow)
+
+ /** Tests */
+ testImplementation(deps.test.junit5)
+ testRuntimeOnly(deps.test.junit5.engine)
+ testImplementation(deps.test.mockk)
+ testImplementation(deps.test.truth)
+ testImplementation(deps.test.coroutine)
+}
\ No newline at end of file
diff --git a/features/survey/impl/src/main/kotlin/com/tangem/features/survey/impl/DefaultSurveyComponent.kt b/features/survey/impl/src/main/kotlin/com/tangem/features/survey/impl/DefaultSurveyComponent.kt
new file mode 100644
index 0000000000..bc06782acc
--- /dev/null
+++ b/features/survey/impl/src/main/kotlin/com/tangem/features/survey/impl/DefaultSurveyComponent.kt
@@ -0,0 +1,73 @@
+package com.tangem.features.survey.impl
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.tangem.core.analytics.api.AnalyticsEventHandler
+import com.tangem.core.decompose.context.AppComponentContext
+import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
+import com.tangem.features.survey.SurveyComponent
+import com.tangem.features.survey.SurveyLaunchData
+import com.tangem.features.survey.SurveySparrowLauncher
+import com.tangem.features.survey.impl.service.SurveyCustomParamsBuilder
+import com.tangem.utils.logging.TangemLogger
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.launch
+
+@Suppress("LongParameterList")
+internal class DefaultSurveyComponent @AssistedInject constructor(
+ @Assisted appComponentContext: AppComponentContext,
+ @Assisted private val params: SurveyComponent.Params,
+ private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
+ private val customParamsBuilder: SurveyCustomParamsBuilder,
+ private val surveySparrowLauncher: SurveySparrowLauncher,
+ @Suppress("UnusedPrivateProperty") // TODO([REDACTED_TASK_KEY]): emit [Survey] analytics events
+ private val analyticsEventHandler: AnalyticsEventHandler,
+) : SurveyComponent, AppComponentContext by appComponentContext {
+
+ init {
+ // componentScope runs on mainImmediate, so presenting the SDK is already on the main thread.
+ componentScope.launch {
+ val launchData = buildLaunchData()
+ if (launchData != null) {
+ surveySparrowLauncher.present(activity, launchData)
+ // TODO([REDACTED_TASK_KEY]): analyticsEventHandler.send(SurveyAnalyticsEvent.Shown(...))
+ }
+ router.pop()
+ }
+ }
+
+ private suspend fun buildLaunchData(): SurveyLaunchData? {
+ return getSelectedWalletSyncUseCase().fold(
+ ifLeft = { error ->
+ TangemLogger.e("$TAG: survey skipped, no available wallet ($error)")
+ null
+ },
+ ifRight = { userWallet ->
+ SurveyLaunchData(
+ domain = SURVEY_DOMAIN,
+ token = params.token,
+ customParams = customParamsBuilder.build(
+ userWallet = userWallet,
+ token = params.token,
+ displayId = params.displayId,
+ ),
+ )
+ },
+ )
+ }
+
+ @Composable
+ override fun Content(modifier: Modifier) = Unit
+
+ @AssistedFactory
+ interface Factory : SurveyComponent.Factory {
+ override fun create(context: AppComponentContext, params: SurveyComponent.Params): DefaultSurveyComponent
+ }
+
+ private companion object {
+ const val TAG = "SurveyComponent"
+ const val SURVEY_DOMAIN = "tangem.surveysparrow.com"
+ }
+}
\ No newline at end of file
diff --git a/features/survey/impl/src/main/kotlin/com/tangem/features/survey/impl/DefaultSurveyFeatureToggles.kt b/features/survey/impl/src/main/kotlin/com/tangem/features/survey/impl/DefaultSurveyFeatureToggles.kt
new file mode 100644
index 0000000000..6959c5bc9e
--- /dev/null
+++ b/features/survey/impl/src/main/kotlin/com/tangem/features/survey/impl/DefaultSurveyFeatureToggles.kt
@@ -0,0 +1,14 @@
+package com.tangem.features.survey.impl
+
+import com.tangem.core.configtoggle.FeatureToggles
+import com.tangem.core.configtoggle.feature.FeatureTogglesManager
+import com.tangem.features.survey.SurveyFeatureToggles
+import javax.inject.Inject
+
+internal class DefaultSurveyFeatureToggles @Inject constructor(
+ private val featureTogglesManager: FeatureTogglesManager,
+) : SurveyFeatureToggles {
+
+ override val areSurveysEnabled: Boolean
+ get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_15482_SURVEYSPARROW_ENABLED)
+}
\ No newline at end of file
diff --git a/features/survey/impl/src/main/kotlin/com/tangem/features/survey/impl/DefaultSurveySparrowLauncher.kt b/features/survey/impl/src/main/kotlin/com/tangem/features/survey/impl/DefaultSurveySparrowLauncher.kt
new file mode 100644
index 0000000000..eb648e78b8
--- /dev/null
+++ b/features/survey/impl/src/main/kotlin/com/tangem/features/survey/impl/DefaultSurveySparrowLauncher.kt
@@ -0,0 +1,38 @@
+package com.tangem.features.survey.impl
+
+import android.app.Activity
+import com.surveysparrow.ss_android_sdk.SsSurvey
+import com.surveysparrow.ss_android_sdk.SurveySparrow
+import com.tangem.features.survey.SurveyLaunchData
+import com.tangem.features.survey.SurveySparrowLauncher
+import com.tangem.utils.logging.TangemLogger
+import javax.inject.Inject
+
+internal class DefaultSurveySparrowLauncher @Inject constructor() : SurveySparrowLauncher {
+
+ override fun present(activity: Activity, data: SurveyLaunchData) {
+ if (activity.isFinishing || activity.isDestroyed) {
+ TangemLogger.e("$TAG: cannot present survey, activity is finishing/destroyed")
+ return
+ }
+
+ val survey = try {
+ SsSurvey(data.domain, data.token).apply {
+ setSurveyType(SurveySparrow.CLASSIC)
+ data.customParams.forEach { (key, value) -> addCustomParam(key, value) }
+ }
+ } catch (e: Exception) {
+ TangemLogger.e("$TAG: failed to create SurveySparrow survey", e)
+ return
+ }
+
+ // Result handling (onActivityResult -> [Survey] Completed/Dismissed) is planned in [REDACTED_TASK_KEY]
+ SurveySparrow(activity, survey).startSurveyForResult(SURVEY_REQUEST_CODE)
+ TangemLogger.d("$TAG: survey started (requestCode=$SURVEY_REQUEST_CODE)")
+ }
+
+ private companion object {
+ const val TAG = "SurveySparrowPresenter"
+ const val SURVEY_REQUEST_CODE = 1001
+ }
+}
\ No newline at end of file
diff --git a/features/survey/impl/src/main/kotlin/com/tangem/features/survey/impl/deeplink/DefaultSurveyDeepLinkHandler.kt b/features/survey/impl/src/main/kotlin/com/tangem/features/survey/impl/deeplink/DefaultSurveyDeepLinkHandler.kt
new file mode 100644
index 0000000000..013015eb8a
--- /dev/null
+++ b/features/survey/impl/src/main/kotlin/com/tangem/features/survey/impl/deeplink/DefaultSurveyDeepLinkHandler.kt
@@ -0,0 +1,47 @@
+package com.tangem.features.survey.impl.deeplink
+
+import com.tangem.common.routing.AppRoute
+import com.tangem.common.routing.AppRouter
+import com.tangem.features.survey.SurveyFeatureToggles
+import com.tangem.features.survey.deeplink.SurveyDeepLinkHandler
+import com.tangem.utils.logging.TangemLogger
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+internal class DefaultSurveyDeepLinkHandler @AssistedInject constructor(
+ @Assisted private val queryParams: Map,
+ private val surveyFeatureToggles: SurveyFeatureToggles,
+ private val appRouter: AppRouter,
+) : SurveyDeepLinkHandler {
+
+ init {
+ handleDeepLink()
+ }
+
+ private fun handleDeepLink() {
+ if (!surveyFeatureToggles.areSurveysEnabled) {
+ TangemLogger.i("$TAG: survey deeplink ignored, feature is disabled")
+ return
+ }
+
+ val token = queryParams[QUERY_TOKEN]?.takeIf { it.isNotBlank() }
+ if (token == null) {
+ TangemLogger.e("$TAG: survey deeplink ignored, missing 'token' query param")
+ return
+ }
+
+ appRouter.push(AppRoute.Survey(token = token, displayId = queryParams[QUERY_DISPLAY_ID]))
+ }
+
+ @AssistedFactory
+ interface Factory : SurveyDeepLinkHandler.Factory {
+ override fun create(queryParams: Map): DefaultSurveyDeepLinkHandler
+ }
+
+ private companion object {
+ const val TAG = "SurveyDeepLink"
+ const val QUERY_TOKEN = "token"
+ const val QUERY_DISPLAY_ID = "display_id"
+ }
+}
\ No newline at end of file
diff --git a/features/survey/impl/src/main/kotlin/com/tangem/features/survey/impl/di/SurveyModule.kt b/features/survey/impl/src/main/kotlin/com/tangem/features/survey/impl/di/SurveyModule.kt
new file mode 100644
index 0000000000..2af50ede8b
--- /dev/null
+++ b/features/survey/impl/src/main/kotlin/com/tangem/features/survey/impl/di/SurveyModule.kt
@@ -0,0 +1,36 @@
+package com.tangem.features.survey.impl.di
+
+import com.tangem.features.survey.SurveyComponent
+import com.tangem.features.survey.SurveyFeatureToggles
+import com.tangem.features.survey.SurveySparrowLauncher
+import com.tangem.features.survey.deeplink.SurveyDeepLinkHandler
+import com.tangem.features.survey.impl.DefaultSurveyComponent
+import com.tangem.features.survey.impl.DefaultSurveyFeatureToggles
+import com.tangem.features.survey.impl.DefaultSurveySparrowLauncher
+import com.tangem.features.survey.impl.deeplink.DefaultSurveyDeepLinkHandler
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal interface SurveyModule {
+
+ @Binds
+ @Singleton
+ fun bindSurveyFeatureToggles(impl: DefaultSurveyFeatureToggles): SurveyFeatureToggles
+
+ @Binds
+ @Singleton
+ fun bindSurveySparrowLauncher(impl: DefaultSurveySparrowLauncher): SurveySparrowLauncher
+
+ @Binds
+ @Singleton
+ fun bindSurveyComponentFactory(impl: DefaultSurveyComponent.Factory): SurveyComponent.Factory
+
+ @Binds
+ @Singleton
+ fun bindSurveyDeepLinkHandlerFactory(impl: DefaultSurveyDeepLinkHandler.Factory): SurveyDeepLinkHandler.Factory
+}
\ No newline at end of file
diff --git a/features/survey/impl/src/main/kotlin/com/tangem/features/survey/impl/service/SurveyCustomParamsBuilder.kt b/features/survey/impl/src/main/kotlin/com/tangem/features/survey/impl/service/SurveyCustomParamsBuilder.kt
new file mode 100644
index 0000000000..d5d905d0b4
--- /dev/null
+++ b/features/survey/impl/src/main/kotlin/com/tangem/features/survey/impl/service/SurveyCustomParamsBuilder.kt
@@ -0,0 +1,48 @@
+package com.tangem.features.survey.impl.service
+
+import com.tangem.common.extensions.calculateSha256
+import com.tangem.common.extensions.hexToBytes
+import com.tangem.common.extensions.toHexString
+import com.tangem.core.analytics.AppInstanceIdProvider
+import com.tangem.datasource.api.tangemTech.models.WalletType
+import com.tangem.domain.models.wallet.UserWallet
+import com.tangem.utils.SupportedLanguages
+import com.tangem.utils.info.AppInfoProvider
+import javax.inject.Inject
+
+internal class SurveyCustomParamsBuilder @Inject constructor(
+ private val appInstanceIdProvider: AppInstanceIdProvider,
+ private val appInfoProvider: AppInfoProvider,
+) {
+
+ suspend fun build(userWallet: UserWallet, token: String, displayId: String?): Map {
+ return buildMap {
+ put(KEY_SURVEY_KEY, token)
+ put(KEY_WALLET_ID, hashWalletId(userWallet))
+ WalletType.from(userWallet)?.let { put(KEY_WALLET_TYPE, it.name.lowercase()) }
+ displayId?.takeIf { it.isNotBlank() }?.let { put(KEY_DISPLAY_ID, it) }
+ appInstanceIdProvider.getAppInstanceId()?.let { put(KEY_DEVICE_ID, it) }
+ put(KEY_PLATFORM, appInfoProvider.platform.lowercase())
+ put(KEY_APP_VERSION, appInfoProvider.appVersion)
+ put(KEY_LANGUAGE, SupportedLanguages.getCurrentSupportedLanguageCode())
+ }
+ }
+
+ private fun hashWalletId(userWallet: UserWallet): String {
+ return userWallet.walletId.stringValue
+ .hexToBytes()
+ .calculateSha256()
+ .toHexString()
+ }
+
+ private companion object {
+ const val KEY_SURVEY_KEY = "survey_key"
+ const val KEY_WALLET_ID = "wallet_id"
+ const val KEY_WALLET_TYPE = "wallet_type"
+ const val KEY_DISPLAY_ID = "display_id"
+ const val KEY_DEVICE_ID = "device_id"
+ const val KEY_PLATFORM = "platform"
+ const val KEY_APP_VERSION = "app_version"
+ const val KEY_LANGUAGE = "language"
+ }
+}
\ No newline at end of file
diff --git a/features/survey/impl/src/test/kotlin/com/tangem/features/survey/impl/deeplink/DefaultSurveyDeepLinkHandlerTest.kt b/features/survey/impl/src/test/kotlin/com/tangem/features/survey/impl/deeplink/DefaultSurveyDeepLinkHandlerTest.kt
new file mode 100644
index 0000000000..e3c1345e03
--- /dev/null
+++ b/features/survey/impl/src/test/kotlin/com/tangem/features/survey/impl/deeplink/DefaultSurveyDeepLinkHandlerTest.kt
@@ -0,0 +1,81 @@
+package com.tangem.features.survey.impl.deeplink
+
+import com.tangem.common.routing.AppRoute
+import com.tangem.common.routing.AppRouter
+import com.tangem.features.survey.SurveyFeatureToggles
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.unmockkObject
+import io.mockk.verify
+import com.tangem.utils.logging.TangemLogger
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+internal class DefaultSurveyDeepLinkHandlerTest {
+
+ private val featureToggles = mockk()
+ private val appRouter = mockk(relaxed = true)
+
+ @BeforeEach
+ fun setup() {
+ mockkObject(TangemLogger)
+ every { TangemLogger.i(any()) } just Runs
+ every { TangemLogger.e(any()) } just Runs
+ }
+
+ @AfterEach
+ fun tearDown() {
+ unmockkObject(TangemLogger)
+ }
+
+ @Test
+ fun `does not navigate when feature is disabled`() {
+ every { featureToggles.areSurveysEnabled } returns false
+
+ createHandler(mapOf("token" to TOKEN, "display_id" to DISPLAY_ID))
+
+ verify(exactly = 0) { appRouter.push(any(), any()) }
+ }
+
+ @Test
+ fun `does not navigate when token is missing`() {
+ every { featureToggles.areSurveysEnabled } returns true
+
+ createHandler(emptyMap())
+
+ verify(exactly = 0) { appRouter.push(any(), any()) }
+ }
+
+ @Test
+ fun `pushes survey route with token and display id on happy path`() {
+ every { featureToggles.areSurveysEnabled } returns true
+
+ createHandler(mapOf("token" to TOKEN, "display_id" to DISPLAY_ID))
+
+ verify { appRouter.push(route = AppRoute.Survey(token = TOKEN, displayId = DISPLAY_ID), onComplete = any()) }
+ }
+
+ @Test
+ fun `pushes survey route with null display id when absent`() {
+ every { featureToggles.areSurveysEnabled } returns true
+
+ createHandler(mapOf("token" to TOKEN))
+
+ verify { appRouter.push(route = AppRoute.Survey(token = TOKEN, displayId = null), onComplete = any()) }
+ }
+
+ private fun createHandler(queryParams: Map) = DefaultSurveyDeepLinkHandler(
+ queryParams = queryParams,
+ surveyFeatureToggles = featureToggles,
+ appRouter = appRouter,
+ )
+
+ private companion object {
+ const val TOKEN = "ntt-84iF22PDajmervYneMW4kv"
+ const val DISPLAY_ID = "42"
+ }
+}
\ No newline at end of file
diff --git a/features/survey/impl/src/test/kotlin/com/tangem/features/survey/impl/service/SurveyCustomParamsBuilderTest.kt b/features/survey/impl/src/test/kotlin/com/tangem/features/survey/impl/service/SurveyCustomParamsBuilderTest.kt
new file mode 100644
index 0000000000..290c70adca
--- /dev/null
+++ b/features/survey/impl/src/test/kotlin/com/tangem/features/survey/impl/service/SurveyCustomParamsBuilderTest.kt
@@ -0,0 +1,104 @@
+package com.tangem.features.survey.impl.service
+
+import com.google.common.truth.Truth.assertThat
+import com.tangem.common.extensions.calculateSha256
+import com.tangem.common.extensions.hexToBytes
+import com.tangem.common.extensions.toHexString
+import com.tangem.core.analytics.AppInstanceIdProvider
+import com.tangem.domain.models.wallet.UserWallet
+import com.tangem.domain.models.wallet.UserWalletId
+import com.tangem.utils.info.AppInfoProvider
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import java.util.Locale
+
+internal class SurveyCustomParamsBuilderTest {
+
+ private val appInstanceIdProvider = mockk()
+ private val appInfoProvider = mockk()
+
+ private val builder = SurveyCustomParamsBuilder(
+ appInstanceIdProvider = appInstanceIdProvider,
+ appInfoProvider = appInfoProvider,
+ )
+
+ @BeforeEach
+ fun setup() {
+ Locale.setDefault(Locale.ENGLISH)
+ every { appInfoProvider.platform } returns "Android"
+ every { appInfoProvider.appVersion } returns "5.40"
+ coEvery { appInstanceIdProvider.getAppInstanceId() } returns "device-123"
+ }
+
+ @Test
+ fun `builds all params for a cold wallet`() = runTest {
+ val wallet = coldWallet(WALLET_ID_HEX)
+
+ val params = builder.build(userWallet = wallet, token = TOKEN, displayId = "42")
+
+ assertThat(params).containsExactlyEntriesIn(
+ mapOf(
+ "survey_key" to TOKEN,
+ "wallet_id" to expectedWalletIdHash(WALLET_ID_HEX),
+ "wallet_type" to "cold",
+ "display_id" to "42",
+ "device_id" to "device-123",
+ "platform" to "android",
+ "app_version" to "5.40",
+ "language" to "en",
+ ),
+ )
+ }
+
+ @Test
+ fun `wallet_id hash is uppercase hex`() = runTest {
+ val params = builder.build(userWallet = coldWallet(WALLET_ID_HEX), token = TOKEN, displayId = null)
+
+ val walletId = params.getValue("wallet_id")
+ assertThat(walletId).isEqualTo(walletId.uppercase())
+ assertThat(walletId).matches("[0-9A-F]+")
+ }
+
+ @Test
+ fun `wallet_type is hot for a hot wallet`() = runTest {
+ val wallet = mockk { every { walletId } returns UserWalletId(WALLET_ID_HEX) }
+
+ val params = builder.build(userWallet = wallet, token = TOKEN, displayId = null)
+
+ assertThat(params["wallet_type"]).isEqualTo("hot")
+ }
+
+ @Test
+ fun `device_id is omitted when app instance id is null`() = runTest {
+ coEvery { appInstanceIdProvider.getAppInstanceId() } returns null
+
+ val params = builder.build(userWallet = coldWallet(WALLET_ID_HEX), token = TOKEN, displayId = "42")
+
+ assertThat(params).doesNotContainKey("device_id")
+ }
+
+ @Test
+ fun `display_id is omitted when null or blank`() = runTest {
+ val nullCase = builder.build(userWallet = coldWallet(WALLET_ID_HEX), token = TOKEN, displayId = null)
+ val blankCase = builder.build(userWallet = coldWallet(WALLET_ID_HEX), token = TOKEN, displayId = " ")
+
+ assertThat(nullCase).doesNotContainKey("display_id")
+ assertThat(blankCase).doesNotContainKey("display_id")
+ }
+
+ private fun coldWallet(walletIdHex: String): UserWallet.Cold = mockk {
+ every { walletId } returns UserWalletId(walletIdHex)
+ }
+
+ private fun expectedWalletIdHash(walletIdHex: String): String =
+ walletIdHex.hexToBytes().calculateSha256().toHexString()
+
+ private companion object {
+ const val TOKEN = "ntt-84iF22PDajmervYneMW4kv"
+ const val WALLET_ID_HEX = "0123456789ABCDEF"
+ }
+}
\ No newline at end of file
diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts
index f96c63f4b4..b3e1f1c4b4 100644
--- a/features/tester/impl/build.gradle.kts
+++ b/features/tester/impl/build.gradle.kts
@@ -47,7 +47,6 @@ dependencies {
/** Other libraries */
implementation(deps.arrow.core)
implementation(deps.kotlin.immutable.collections)
- implementation(deps.surveysparrow)
/** Core modules */
implementation(projects.core.datasource)
@@ -60,6 +59,7 @@ dependencies {
/** Feature Apis */
implementation(projects.features.tester.api)
implementation(projects.features.pushNotifications.api)
+ implementation(projects.features.survey.api)
/* SDK */
implementation(tangemDeps.blockchain)
diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt
index 3829e306b7..a1ede089f5 100644
--- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt
+++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt
@@ -1,6 +1,5 @@
package com.tangem.feature.tester.presentation
-import android.widget.Toast
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -18,7 +17,6 @@ import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeActivity
-import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.feature.tester.presentation.accounts.ui.AccountsScreen
import com.tangem.feature.tester.presentation.accounts.viewmodel.TesterAccountsViewModel
import com.tangem.feature.tester.presentation.actions.TesterActionsScreen
@@ -40,9 +38,10 @@ import com.tangem.feature.tester.presentation.providers.ui.BlockchainProvidersSc
import com.tangem.feature.tester.presentation.providers.viewmodel.BlockchainProvidersViewModel
import com.tangem.feature.tester.presentation.storybook.ui.StoryBookScreen
import com.tangem.feature.tester.presentation.storybook.viewmodel.StoryBookViewModel
-import com.tangem.feature.tester.presentation.surveysparrow.SurveySparrowManager
import com.tangem.feature.tester.presentation.testpush.ui.TestPushScreen
import com.tangem.feature.tester.presentation.testpush.viewmodel.TestPushViewModel
+import com.tangem.features.survey.SurveyLaunchData
+import com.tangem.features.survey.SurveySparrowLauncher
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.collections.immutable.persistentSetOf
import javax.inject.Inject
@@ -64,7 +63,7 @@ internal class TesterActivity : ComposeActivity() {
lateinit var appRouter: AppRouter
@Inject
- lateinit var environmentConfig: EnvironmentConfig
+ lateinit var surveySparrowLauncher: SurveySparrowLauncher
@Composable
override fun ScreenContent(modifier: Modifier) {
@@ -217,29 +216,16 @@ internal class TesterActivity : ComposeActivity() {
}
private fun startSurveySparrow(): Boolean {
- val token = environmentConfig.surveySparrowToken
-
- if (token.isNullOrEmpty()) {
- val toast = Toast.makeText(
- this,
- "Survey Sparrow is not configured. Token is missing.",
- Toast.LENGTH_LONG,
- )
-
- toast.show()
- return false
- }
-
- SurveySparrowManager(domain = DOMAIN, token = token).startSurveyForResult(
+ surveySparrowLauncher.present(
activity = this,
- requestCode = SURVEY_SPARROW_REQUEST_CODE,
+ data = SurveyLaunchData(domain = DOMAIN, token = TEST_SHARE_TOKEN, customParams = emptyMap()),
)
return true
}
private companion object {
- const val DOMAIN = "tangem.com"
- const val SURVEY_SPARROW_REQUEST_CODE = 1001
+ const val DOMAIN = "tangem.surveysparrow.com"
+ const val TEST_SHARE_TOKEN = "ntt-84iF22PDajmervYneMW4kv"
}
}
\ No newline at end of file
diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/surveysparrow/SurveySparrowManager.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/surveysparrow/SurveySparrowManager.kt
deleted file mode 100644
index 3b2385900c..0000000000
--- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/surveysparrow/SurveySparrowManager.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package com.tangem.feature.tester.presentation.surveysparrow
-
-import android.app.Activity
-import com.surveysparrow.ss_android_sdk.SsSurvey
-import com.surveysparrow.ss_android_sdk.SurveySparrow
-import com.tangem.utils.logging.TangemLogger
-
-/**
- * Manager for Survey Sparrow SDK.
- *
- * @param domain Survey Sparrow domain (e.g., "yourcompany")
- * @param token Survey Sparrow SDK token
- */
-class SurveySparrowManager(
- private val domain: String,
- private val token: String,
-) {
-
- /**
- * Create a SurveySparrow instance to start a survey.
- *
- * @param activity The activity context
- * @param customVariables Optional custom variables to pass to the survey
- * @return SurveySparrow instance ready to start
- */
- fun createSurvey(activity: Activity, customVariables: Map? = null): SurveySparrow? {
- return try {
- val survey = SsSurvey(domain, token).apply {
- customVariables?.forEach { (key, value) ->
- addCustomParam(key, value)
- }
- }
-
- SurveySparrow(activity, survey)
- } catch (e: Exception) {
- TangemLogger.e("Failed to create SurveySparrow survey", e)
- null
- }
- }
-
- /**
- * Start a survey for result.
- *
- * @param activity The activity context
- * @param requestCode The request code for onActivityResult
- * @param customVariables Optional custom variables to pass to the survey
- */
- fun startSurveyForResult(activity: Activity, requestCode: Int, customVariables: Map? = null) {
- val surveySparrow = createSurvey(activity, customVariables)
- if (surveySparrow != null) {
- surveySparrow.startSurveyForResult(requestCode)
- TangemLogger.d("SurveySparrow survey started with requestCode: $requestCode")
- }
- }
-}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 4c1a0fb251..590257dd05 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -259,6 +259,9 @@ include(":features:rating:impl")
include(":features:stories:api")
include(":features:stories:impl")
+include(":features:survey:api")
+include(":features:survey:impl")
+
include(":features:txhistory:api")
include(":features:txhistory:impl")