Updated on 2026-08-14
This commit is contained in:
parent
bb5d6848d9
commit
0ee2f48898
29 changed files with 660 additions and 82 deletions
|
|
@ -283,6 +283,8 @@ dependencies {
|
||||||
implementation(projects.features.onboardingV2.impl)
|
implementation(projects.features.onboardingV2.impl)
|
||||||
implementation(projects.features.stories.api)
|
implementation(projects.features.stories.api)
|
||||||
implementation(projects.features.stories.impl)
|
implementation(projects.features.stories.impl)
|
||||||
|
implementation(projects.features.survey.api)
|
||||||
|
implementation(projects.features.survey.impl)
|
||||||
implementation(projects.features.txhistory.api)
|
implementation(projects.features.txhistory.api)
|
||||||
implementation(projects.features.txhistory.impl)
|
implementation(projects.features.txhistory.impl)
|
||||||
implementation(projects.features.biometry.api)
|
implementation(projects.features.biometry.api)
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,17 @@
|
||||||
android:scheme="tangem" />
|
android:scheme="tangem" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="survey"
|
||||||
|
android:scheme="tangem" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.tangem.common.routing.DeepLinkScheme
|
import com.tangem.common.routing.DeepLinkScheme
|
||||||
import com.tangem.common.uri.ExternalUrlValidator
|
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.deeplink.DeeplinkLauncher
|
||||||
import com.tangem.core.navigation.url.UrlOpener
|
import com.tangem.core.navigation.url.UrlOpener
|
||||||
import com.tangem.utils.logging.TangemLogger
|
import com.tangem.utils.logging.TangemLogger
|
||||||
|
|
@ -17,6 +19,7 @@ import com.tangem.utils.logging.TangemLogger
|
||||||
internal class DefaultDeeplinkLauncher(
|
internal class DefaultDeeplinkLauncher(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val urlOpener: UrlOpener,
|
private val urlOpener: UrlOpener,
|
||||||
|
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||||
) : DeeplinkLauncher {
|
) : DeeplinkLauncher {
|
||||||
|
|
||||||
override fun launch(link: String) {
|
override fun launch(link: String) {
|
||||||
|
|
@ -58,11 +61,33 @@ internal class DefaultDeeplinkLauncher(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchDeepLink(uri: Uri) {
|
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 {
|
private fun createDeepLinkIntent(uri: Uri): Intent = Intent(Intent.ACTION_VIEW, uri).apply {
|
||||||
setPackage(context.packageName)
|
setPackage(context.packageName)
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class UnresolvedDeeplinkException(uri: Uri) :
|
||||||
|
RuntimeException("Deeplink has no matching activity: scheme=${uri.scheme}, host=${uri.host}")
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.tap.di
|
package com.tangem.tap.di
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||||
import com.tangem.tap.common.deeplink.DefaultDeeplinkLauncher
|
import com.tangem.tap.common.deeplink.DefaultDeeplinkLauncher
|
||||||
import com.tangem.core.navigation.deeplink.DeeplinkLauncher
|
import com.tangem.core.navigation.deeplink.DeeplinkLauncher
|
||||||
import com.tangem.core.navigation.finisher.AppFinisher
|
import com.tangem.core.navigation.finisher.AppFinisher
|
||||||
|
|
@ -55,7 +56,10 @@ internal interface UtilsModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideDeeplinkLauncher(@ApplicationContext context: Context, urlOpener: UrlOpener): DeeplinkLauncher =
|
fun provideDeeplinkLauncher(
|
||||||
DefaultDeeplinkLauncher(context, urlOpener)
|
@ApplicationContext context: Context,
|
||||||
|
urlOpener: UrlOpener,
|
||||||
|
analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||||
|
): DeeplinkLauncher = DefaultDeeplinkLauncher(context, urlOpener, analyticsExceptionHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ import com.tangem.features.feed.entry.components.FeedEntryRoute
|
||||||
import com.tangem.features.home.api.HomeComponent
|
import com.tangem.features.home.api.HomeComponent
|
||||||
import com.tangem.features.hotwallet.*
|
import com.tangem.features.hotwallet.*
|
||||||
import com.tangem.features.kyc.KycComponent
|
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.ChooseManagedTokensComponent
|
||||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||||
import com.tangem.features.managetokens.component.ManageTokensMode
|
import com.tangem.features.managetokens.component.ManageTokensMode
|
||||||
|
|
@ -112,6 +113,7 @@ internal class ChildFactory @Inject constructor(
|
||||||
private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory,
|
private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory,
|
||||||
private val tangemPayWalletOnboardingComponentFactory: TangemPayHotWalletOnboardingComponent.Factory,
|
private val tangemPayWalletOnboardingComponentFactory: TangemPayHotWalletOnboardingComponent.Factory,
|
||||||
private val kycComponentFactory: KycComponent.Factory,
|
private val kycComponentFactory: KycComponent.Factory,
|
||||||
|
private val surveyComponentFactory: SurveyComponent.Factory,
|
||||||
private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
|
private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
|
||||||
private val feedEntryComponentFactory: FeedEntryComponent.Factory,
|
private val feedEntryComponentFactory: FeedEntryComponent.Factory,
|
||||||
private val addFundsComponentFactory: AddFundsComponent.Factory,
|
private val addFundsComponentFactory: AddFundsComponent.Factory,
|
||||||
|
|
@ -702,6 +704,13 @@ internal class ChildFactory @Inject constructor(
|
||||||
componentFactory = kycComponentFactory,
|
componentFactory = kycComponentFactory,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is AppRoute.Survey -> {
|
||||||
|
createComponentChild(
|
||||||
|
context = context,
|
||||||
|
params = SurveyComponent.Params(token = route.token, displayId = route.displayId),
|
||||||
|
componentFactory = surveyComponentFactory,
|
||||||
|
)
|
||||||
|
}
|
||||||
is AppRoute.YieldSupplyEntry -> {
|
is AppRoute.YieldSupplyEntry -> {
|
||||||
createComponentChild(
|
createComponentChild(
|
||||||
context = context,
|
context = context,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import com.tangem.features.onramp.deeplink.SellDeepLinkHandler
|
||||||
import com.tangem.features.onramp.deeplink.SwapDeepLinkHandler
|
import com.tangem.features.onramp.deeplink.SwapDeepLinkHandler
|
||||||
import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler
|
import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler
|
||||||
import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler
|
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.OnboardVisaDeepLinkHandler
|
||||||
import com.tangem.features.tangempay.deeplink.TangemPayMainDeepLinkHandler
|
import com.tangem.features.tangempay.deeplink.TangemPayMainDeepLinkHandler
|
||||||
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
|
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
|
||||||
|
|
@ -62,6 +63,7 @@ internal class DeepLinkFactory @Inject constructor(
|
||||||
private val newsDeepLink: NewsDeepLinkHandler.Factory,
|
private val newsDeepLink: NewsDeepLinkHandler.Factory,
|
||||||
private val earnDeepLink: EarnDeepLinkHandler.Factory,
|
private val earnDeepLink: EarnDeepLinkHandler.Factory,
|
||||||
private val yieldDeepLink: YieldDeepLinkHandler.Factory,
|
private val yieldDeepLink: YieldDeepLinkHandler.Factory,
|
||||||
|
private val surveyDeepLink: SurveyDeepLinkHandler.Factory,
|
||||||
) {
|
) {
|
||||||
private val permittedAppRoute = MutableStateFlow(false)
|
private val permittedAppRoute = MutableStateFlow(false)
|
||||||
|
|
||||||
|
|
@ -175,6 +177,7 @@ internal class DeepLinkFactory @Inject constructor(
|
||||||
DeepLinkRoute.Earn.host -> earnDeepLink.create(queryParams)
|
DeepLinkRoute.Earn.host -> earnDeepLink.create(queryParams)
|
||||||
DeepLinkRoute.Yield.host -> yieldDeepLink.create(coroutineScope, queryParams)
|
DeepLinkRoute.Yield.host -> yieldDeepLink.create(coroutineScope, queryParams)
|
||||||
DeepLinkRoute.PayAppMain.host -> tangemPayMainDeepLink.create(coroutineScope, queryParams)
|
DeepLinkRoute.PayAppMain.host -> tangemPayMainDeepLink.create(coroutineScope, queryParams)
|
||||||
|
DeepLinkRoute.Survey.host -> surveyDeepLink.create(queryParams)
|
||||||
else -> {
|
else -> {
|
||||||
TangemLogger.i(
|
TangemLogger.i(
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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.NewsDeepLinkHandler
|
||||||
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
|
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
|
||||||
import com.tangem.features.feed.entry.deeplink.YieldDeepLinkHandler
|
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.BuyDeepLinkHandler
|
||||||
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
|
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
|
||||||
import com.tangem.features.onramp.deeplink.SellDeepLinkHandler
|
import com.tangem.features.onramp.deeplink.SellDeepLinkHandler
|
||||||
|
|
@ -99,6 +100,10 @@ class DeepLinkFactoryTest {
|
||||||
every { create(any()) } returns mockk()
|
every { create(any()) } returns mockk()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val surveyDeepLinkFactory = mockk<SurveyDeepLinkHandler.Factory>(relaxed = true) {
|
||||||
|
every { create(any()) } returns mockk()
|
||||||
|
}
|
||||||
|
|
||||||
private val earnDeepLinkFactory = mockk<EarnDeepLinkHandler.Factory>(relaxed = true) {
|
private val earnDeepLinkFactory = mockk<EarnDeepLinkHandler.Factory>(relaxed = true) {
|
||||||
every { create(any()) } returns mockk()
|
every { create(any()) } returns mockk()
|
||||||
}
|
}
|
||||||
|
|
@ -140,6 +145,7 @@ class DeepLinkFactoryTest {
|
||||||
newsDeepLink = newsDeepLinkFactory,
|
newsDeepLink = newsDeepLinkFactory,
|
||||||
earnDeepLink = earnDeepLinkFactory,
|
earnDeepLink = earnDeepLinkFactory,
|
||||||
yieldDeepLink = yieldDeepLinkFactory,
|
yieldDeepLink = yieldDeepLinkFactory,
|
||||||
|
surveyDeepLink = surveyDeepLinkFactory,
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
|
||||||
|
|
@ -498,6 +498,9 @@ sealed class AppRoute(val path: String) : Route {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Kyc(val userWalletId: UserWalletId) : AppRoute(path = "/kyc")
|
data class Kyc(val userWalletId: UserWalletId) : AppRoute(path = "/kyc")
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Survey(val token: String, val displayId: String? = null) : AppRoute(path = "/survey")
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class YieldSupplyEntry(
|
data class YieldSupplyEntry(
|
||||||
val userWalletId: UserWalletId,
|
val userWalletId: UserWalletId,
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,10 @@ sealed class DeepLinkRoute {
|
||||||
data object PayAppMain : DeepLinkRoute() {
|
data object PayAppMain : DeepLinkRoute() {
|
||||||
override val host: String = "pay-app-main"
|
override val host: String = "pay-app-main"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data object Survey : DeepLinkRoute() {
|
||||||
|
override val host: String = "survey"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class DeepLinkScheme(val scheme: String) {
|
enum class DeepLinkScheme(val scheme: String) {
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,10 @@
|
||||||
"name": "AND_15438_BACKEND_AUTHENTICATION_ENABLED",
|
"name": "AND_15438_BACKEND_AUTHENTICATION_ENABLED",
|
||||||
"version": "undefined"
|
"version": "undefined"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "AND_15482_SURVEYSPARROW_ENABLED",
|
||||||
|
"version": "undefined"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "AND_15258_QUICK_TOP_UP_ENABLED",
|
"name": "AND_15258_QUICK_TOP_UP_ENABLED",
|
||||||
"version": "undefined"
|
"version": "undefined"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.tangem.features.promobanners.impl.model
|
package com.tangem.features.promobanners.impl.model
|
||||||
|
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.tangem.core.navigation.deeplink.DeeplinkLauncher
|
import com.tangem.core.navigation.deeplink.DeeplinkLauncher
|
||||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.decompose.di.ModelScoped
|
import com.tangem.core.decompose.di.ModelScoped
|
||||||
|
|
@ -118,7 +119,18 @@ internal class PromoBannersBlockModel @Inject constructor(
|
||||||
|
|
||||||
private fun onButtonClick(displayId: Int, deeplink: String?) {
|
private fun onButtonClick(displayId: Int, deeplink: String?) {
|
||||||
analyticsEventHandler.send(PromoBannerAnalyticsEvent.Clicked(displayId, placeholderName))
|
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(
|
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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
14
features/survey/api/build.gradle.kts
Normal file
14
features/survey/api/build.gradle.kts
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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<Params, SurveyComponent>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.tangem.features.survey
|
||||||
|
|
||||||
|
interface SurveyFeatureToggles {
|
||||||
|
|
||||||
|
val areSurveysEnabled: Boolean
|
||||||
|
}
|
||||||
|
|
@ -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<String, String>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.tangem.features.survey.deeplink
|
||||||
|
|
||||||
|
interface SurveyDeepLinkHandler {
|
||||||
|
|
||||||
|
interface Factory {
|
||||||
|
fun create(queryParams: Map<String, String>): SurveyDeepLinkHandler
|
||||||
|
}
|
||||||
|
}
|
||||||
61
features/survey/impl/build.gradle.kts
Normal file
61
features/survey/impl/build.gradle.kts
Normal file
|
|
@ -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<Test>().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)
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String, String>,
|
||||||
|
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<String, String>): DefaultSurveyDeepLinkHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val TAG = "SurveyDeepLink"
|
||||||
|
const val QUERY_TOKEN = "token"
|
||||||
|
const val QUERY_DISPLAY_ID = "display_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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<String, String> {
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<SurveyFeatureToggles>()
|
||||||
|
private val appRouter = mockk<AppRouter>(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<String, String>) = DefaultSurveyDeepLinkHandler(
|
||||||
|
queryParams = queryParams,
|
||||||
|
surveyFeatureToggles = featureToggles,
|
||||||
|
appRouter = appRouter,
|
||||||
|
)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val TOKEN = "ntt-84iF22PDajmervYneMW4kv"
|
||||||
|
const val DISPLAY_ID = "42"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<AppInstanceIdProvider>()
|
||||||
|
private val appInfoProvider = mockk<AppInfoProvider>()
|
||||||
|
|
||||||
|
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<UserWallet.Hot> { 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -47,7 +47,6 @@ dependencies {
|
||||||
/** Other libraries */
|
/** Other libraries */
|
||||||
implementation(deps.arrow.core)
|
implementation(deps.arrow.core)
|
||||||
implementation(deps.kotlin.immutable.collections)
|
implementation(deps.kotlin.immutable.collections)
|
||||||
implementation(deps.surveysparrow)
|
|
||||||
|
|
||||||
/** Core modules */
|
/** Core modules */
|
||||||
implementation(projects.core.datasource)
|
implementation(projects.core.datasource)
|
||||||
|
|
@ -60,6 +59,7 @@ dependencies {
|
||||||
/** Feature Apis */
|
/** Feature Apis */
|
||||||
implementation(projects.features.tester.api)
|
implementation(projects.features.tester.api)
|
||||||
implementation(projects.features.pushNotifications.api)
|
implementation(projects.features.pushNotifications.api)
|
||||||
|
implementation(projects.features.survey.api)
|
||||||
|
|
||||||
/* SDK */
|
/* SDK */
|
||||||
implementation(tangemDeps.blockchain)
|
implementation(tangemDeps.blockchain)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.tangem.feature.tester.presentation
|
package com.tangem.feature.tester.presentation
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
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.UiDependencies
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.screen.ComposeActivity
|
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.ui.AccountsScreen
|
||||||
import com.tangem.feature.tester.presentation.accounts.viewmodel.TesterAccountsViewModel
|
import com.tangem.feature.tester.presentation.accounts.viewmodel.TesterAccountsViewModel
|
||||||
import com.tangem.feature.tester.presentation.actions.TesterActionsScreen
|
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.providers.viewmodel.BlockchainProvidersViewModel
|
||||||
import com.tangem.feature.tester.presentation.storybook.ui.StoryBookScreen
|
import com.tangem.feature.tester.presentation.storybook.ui.StoryBookScreen
|
||||||
import com.tangem.feature.tester.presentation.storybook.viewmodel.StoryBookViewModel
|
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.ui.TestPushScreen
|
||||||
import com.tangem.feature.tester.presentation.testpush.viewmodel.TestPushViewModel
|
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 dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -64,7 +63,7 @@ internal class TesterActivity : ComposeActivity() {
|
||||||
lateinit var appRouter: AppRouter
|
lateinit var appRouter: AppRouter
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var environmentConfig: EnvironmentConfig
|
lateinit var surveySparrowLauncher: SurveySparrowLauncher
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun ScreenContent(modifier: Modifier) {
|
override fun ScreenContent(modifier: Modifier) {
|
||||||
|
|
@ -217,29 +216,16 @@ internal class TesterActivity : ComposeActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startSurveySparrow(): Boolean {
|
private fun startSurveySparrow(): Boolean {
|
||||||
val token = environmentConfig.surveySparrowToken
|
surveySparrowLauncher.present(
|
||||||
|
|
||||||
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(
|
|
||||||
activity = this,
|
activity = this,
|
||||||
requestCode = SURVEY_SPARROW_REQUEST_CODE,
|
data = SurveyLaunchData(domain = DOMAIN, token = TEST_SHARE_TOKEN, customParams = emptyMap()),
|
||||||
)
|
)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val DOMAIN = "tangem.com"
|
const val DOMAIN = "tangem.surveysparrow.com"
|
||||||
const val SURVEY_SPARROW_REQUEST_CODE = 1001
|
const val TEST_SHARE_TOKEN = "ntt-84iF22PDajmervYneMW4kv"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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<String, String>? = 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<String, String>? = null) {
|
|
||||||
val surveySparrow = createSurvey(activity, customVariables)
|
|
||||||
if (surveySparrow != null) {
|
|
||||||
surveySparrow.startSurveyForResult(requestCode)
|
|
||||||
TangemLogger.d("SurveySparrow survey started with requestCode: $requestCode")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -259,6 +259,9 @@ include(":features:rating:impl")
|
||||||
include(":features:stories:api")
|
include(":features:stories:api")
|
||||||
include(":features:stories:impl")
|
include(":features:stories:impl")
|
||||||
|
|
||||||
|
include(":features:survey:api")
|
||||||
|
include(":features:survey:impl")
|
||||||
|
|
||||||
include(":features:txhistory:api")
|
include(":features:txhistory:api")
|
||||||
include(":features:txhistory:impl")
|
include(":features:txhistory:impl")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue