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.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)
|
||||
|
|
|
|||
|
|
@ -210,6 +210,17 @@
|
|||
android:scheme="tangem" />
|
||||
</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>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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<SurveyDeepLinkHandler.Factory>(relaxed = true) {
|
||||
every { create(any()) } returns mockk()
|
||||
}
|
||||
|
||||
private val earnDeepLinkFactory = mockk<EarnDeepLinkHandler.Factory>(relaxed = true) {
|
||||
every { create(any()) } returns mockk()
|
||||
}
|
||||
|
|
@ -140,6 +145,7 @@ class DeepLinkFactoryTest {
|
|||
newsDeepLink = newsDeepLinkFactory,
|
||||
earnDeepLink = earnDeepLinkFactory,
|
||||
yieldDeepLink = yieldDeepLinkFactory,
|
||||
surveyDeepLink = surveyDeepLinkFactory,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
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 */
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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:impl")
|
||||
|
||||
include(":features:survey:api")
|
||||
include(":features:survey:impl")
|
||||
|
||||
include(":features:txhistory:api")
|
||||
include(":features:txhistory:impl")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue