Updated on 2026-08-14
This commit is contained in:
parent
f9f2ac60d0
commit
c76d1de3cf
15 changed files with 211 additions and 84 deletions
|
|
@ -7,7 +7,7 @@ import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||||
import com.tangem.common.core.TangemSdkError
|
import com.tangem.common.core.TangemSdkError
|
||||||
import com.tangem.common.extensions.clickAndWaitFor
|
import com.tangem.common.extensions.clickAndWaitFor
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.domain.card.ScanFailsRequester
|
import com.tangem.core.analytics.models.AnalyticsParam
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.tangem.scenarios.checkFailedTransactionDialog
|
import com.tangem.scenarios.checkFailedTransactionDialog
|
||||||
|
|
@ -179,7 +179,7 @@ class FeedbackTest : BaseTestCase() {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
val requester = store.state.daggerGraphState.scanFailsRequester!!
|
val requester = store.state.daggerGraphState.scanFailsRequester!!
|
||||||
MainScope().launch {
|
MainScope().launch {
|
||||||
requester.show(ScanFailsRequester.Source.MAIN)
|
requester.show(AnalyticsParam.ScreensSources.Main)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,11 @@
|
||||||
package com.tangem.tap.common.redux.global
|
package com.tangem.tap.common.redux.global
|
||||||
|
|
||||||
import com.tangem.common.CompletionResult
|
|
||||||
import com.tangem.core.analytics.models.AnalyticsParam
|
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.models.scan.ScanResponse
|
import com.tangem.domain.models.scan.ScanResponse
|
||||||
import org.rekotlin.Action
|
import org.rekotlin.Action
|
||||||
|
|
||||||
sealed class GlobalAction : Action {
|
sealed class GlobalAction : Action {
|
||||||
|
|
||||||
object ScanFailsCounter {
|
|
||||||
data class ChooseBehavior(
|
|
||||||
val result: CompletionResult<ScanResponse>,
|
|
||||||
val analyticsSource: AnalyticsParam.ScreensSources,
|
|
||||||
) : GlobalAction()
|
|
||||||
|
|
||||||
object Reset : GlobalAction()
|
|
||||||
object Increment : GlobalAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
data class SaveScanResponse(val scanResponse: ScanResponse) : GlobalAction()
|
data class SaveScanResponse(val scanResponse: ScanResponse) : GlobalAction()
|
||||||
|
|
||||||
data class ChangeAppCurrency(val appCurrency: AppCurrency) : GlobalAction()
|
data class ChangeAppCurrency(val appCurrency: AppCurrency) : GlobalAction()
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
package com.tangem.tap.common.redux.global
|
package com.tangem.tap.common.redux.global
|
||||||
|
|
||||||
import com.tangem.common.CompletionResult
|
|
||||||
import com.tangem.common.core.TangemSdkError
|
|
||||||
import com.tangem.core.analytics.models.AnalyticsParam
|
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.card.ScanFailsRequester
|
|
||||||
import com.tangem.domain.models.scan.ScanResponse
|
|
||||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||||
import com.tangem.tap.common.extensions.inject
|
import com.tangem.tap.common.extensions.inject
|
||||||
import com.tangem.tap.common.redux.AppState
|
import com.tangem.tap.common.redux.AppState
|
||||||
|
|
@ -30,43 +25,12 @@ private val globalMiddlewareHandler: Middleware<AppState> = { _, _ ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongMethod", "ComplexMethod")
|
|
||||||
private fun handleAction(action: Action) {
|
private fun handleAction(action: Action) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is GlobalAction.ScanFailsCounter.ChooseBehavior -> {
|
|
||||||
when (action.result) {
|
|
||||||
is CompletionResult.Success -> store.dispatch(GlobalAction.ScanFailsCounter.Reset)
|
|
||||||
is CompletionResult.Failure -> {
|
|
||||||
handleFailureChooseBehaviour(action.result, action.analyticsSource)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is GlobalAction.RestoreAppCurrency -> restoreAppCurrency()
|
is GlobalAction.RestoreAppCurrency -> restoreAppCurrency()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFailureChooseBehaviour(
|
|
||||||
result: CompletionResult.Failure<ScanResponse>,
|
|
||||||
analyticsSource: AnalyticsParam.ScreensSources,
|
|
||||||
) {
|
|
||||||
if (result.error is TangemSdkError.UserCancelled) {
|
|
||||||
store.dispatch(GlobalAction.ScanFailsCounter.Increment)
|
|
||||||
if (store.state.globalState.scanCardFailsCounter >= 2) {
|
|
||||||
val scanFailsSource = when (analyticsSource) {
|
|
||||||
is AnalyticsParam.ScreensSources.SignIn -> ScanFailsRequester.Source.SIGN_IN
|
|
||||||
is AnalyticsParam.ScreensSources.Settings -> ScanFailsRequester.Source.SETTINGS
|
|
||||||
is AnalyticsParam.ScreensSources.Intro -> ScanFailsRequester.Source.INTRO
|
|
||||||
else -> ScanFailsRequester.Source.MAIN
|
|
||||||
}
|
|
||||||
scope.launch {
|
|
||||||
store.inject(DaggerGraphState::scanFailsRequester).show(scanFailsSource)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
store.dispatch(GlobalAction.ScanFailsCounter.Reset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreAppCurrency() {
|
private fun restoreAppCurrency() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val currency = store.inject(DaggerGraphState::appCurrencyRepository)
|
val currency = store.inject(DaggerGraphState::appCurrencyRepository)
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,6 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
|
||||||
val globalState = state.globalState
|
val globalState = state.globalState
|
||||||
|
|
||||||
return when (action) {
|
return when (action) {
|
||||||
is GlobalAction.ScanFailsCounter.Increment -> {
|
|
||||||
globalState.copy(scanCardFailsCounter = globalState.scanCardFailsCounter + 1)
|
|
||||||
}
|
|
||||||
is GlobalAction.ScanFailsCounter.Reset -> {
|
|
||||||
globalState.copy(scanCardFailsCounter = 0)
|
|
||||||
}
|
|
||||||
is GlobalAction.SaveScanResponse -> {
|
is GlobalAction.SaveScanResponse -> {
|
||||||
globalState.copy(scanResponse = action.scanResponse)
|
globalState.copy(scanResponse = action.scanResponse)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ data class GlobalState(
|
||||||
val scanResponse: ScanResponse? = null,
|
val scanResponse: ScanResponse? = null,
|
||||||
val tapWalletManager: TapWalletManager = TapWalletManager(),
|
val tapWalletManager: TapWalletManager = TapWalletManager(),
|
||||||
val appCurrency: AppCurrency = AppCurrency.Default,
|
val appCurrency: AppCurrency = AppCurrency.Default,
|
||||||
val scanCardFailsCounter: Int = 0,
|
|
||||||
val isLastSignWithRing: Boolean = false,
|
val isLastSignWithRing: Boolean = false,
|
||||||
) : StateType
|
) : StateType
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.tangem.tap.domain.scanCard
|
||||||
|
|
||||||
|
import com.tangem.core.analytics.models.AnalyticsParam
|
||||||
|
import com.tangem.domain.card.ScanFailsCounter
|
||||||
|
import com.tangem.domain.card.ScanFailsRequester
|
||||||
|
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
internal class DefaultScanFailsCounter @Inject constructor(
|
||||||
|
private val scanFailsRequester: ScanFailsRequester,
|
||||||
|
private val appScope: AppCoroutineScope,
|
||||||
|
) : ScanFailsCounter {
|
||||||
|
|
||||||
|
private var counter: Int = 0
|
||||||
|
|
||||||
|
override fun reset() {
|
||||||
|
counter = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScanFailure(isUserCancelled: Boolean, source: AnalyticsParam.ScreensSources) {
|
||||||
|
if (isUserCancelled) {
|
||||||
|
counter++
|
||||||
|
if (counter >= THRESHOLD) {
|
||||||
|
appScope.launch { scanFailsRequester.show(source) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
counter = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val THRESHOLD = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ import com.tangem.core.ui.R
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.core.ui.extensions.toWrappedList
|
import com.tangem.core.ui.extensions.toWrappedList
|
||||||
import com.tangem.core.ui.message.dialog.Dialogs
|
import com.tangem.core.ui.message.dialog.Dialogs
|
||||||
|
import com.tangem.domain.card.ScanFailsCounter
|
||||||
import com.tangem.domain.card.common.util.twinsIsTwinned
|
import com.tangem.domain.card.common.util.twinsIsTwinned
|
||||||
import com.tangem.domain.common.extensions.withMainContext
|
import com.tangem.domain.common.extensions.withMainContext
|
||||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||||
|
|
@ -26,9 +27,7 @@ import com.tangem.domain.models.scan.ScanResponse
|
||||||
import com.tangem.sdk.extensions.localizedDescriptionRes
|
import com.tangem.sdk.extensions.localizedDescriptionRes
|
||||||
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
|
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
|
||||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
|
||||||
import com.tangem.tap.common.extensions.inject
|
import com.tangem.tap.common.extensions.inject
|
||||||
import com.tangem.tap.common.redux.global.GlobalAction
|
|
||||||
import com.tangem.tap.features.disclaimer.createDisclaimer
|
import com.tangem.tap.features.disclaimer.createDisclaimer
|
||||||
import com.tangem.tap.features.onboarding.OnboardingHelper
|
import com.tangem.tap.features.onboarding.OnboardingHelper
|
||||||
import com.tangem.tap.mainScope
|
import com.tangem.tap.mainScope
|
||||||
|
|
@ -49,6 +48,7 @@ internal class LegacyScanProcessor @Inject constructor(
|
||||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
private val trackingContextProxy: TrackingContextProxy,
|
private val trackingContextProxy: TrackingContextProxy,
|
||||||
|
private val scanFailsCounter: ScanFailsCounter,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun scan(
|
suspend fun scan(
|
||||||
|
|
@ -89,10 +89,13 @@ internal class LegacyScanProcessor @Inject constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
val analyticsEvent = Basic.CardWasScanned(analyticsSource)
|
val analyticsEvent = Basic.CardWasScanned(analyticsSource)
|
||||||
store.dispatchOnMain(GlobalAction.ScanFailsCounter.ChooseBehavior(result, analyticsSource))
|
|
||||||
|
|
||||||
result
|
result
|
||||||
.doOnFailure { error ->
|
.doOnFailure { error ->
|
||||||
|
scanFailsCounter.onScanFailure(
|
||||||
|
isUserCancelled = error is TangemSdkError.UserCancelled,
|
||||||
|
source = analyticsSource,
|
||||||
|
)
|
||||||
onScanFailure(
|
onScanFailure(
|
||||||
analyticsSource = analyticsSource,
|
analyticsSource = analyticsSource,
|
||||||
error = error,
|
error = error,
|
||||||
|
|
@ -106,6 +109,7 @@ internal class LegacyScanProcessor @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.doOnSuccess { scanResponse ->
|
.doOnSuccess { scanResponse ->
|
||||||
|
scanFailsCounter.reset()
|
||||||
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
|
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
|
||||||
|
|
||||||
sendAnalytics(analyticsEvent, scanResponse)
|
sendAnalytics(analyticsEvent, scanResponse)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import com.tangem.core.analytics.models.AnalyticsParam
|
||||||
import com.tangem.core.analytics.models.Basic
|
import com.tangem.core.analytics.models.Basic
|
||||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||||
import com.tangem.domain.card.ScanCardException
|
import com.tangem.domain.card.ScanCardException
|
||||||
import com.tangem.domain.card.ScanFailsRequester
|
|
||||||
import com.tangem.domain.models.scan.ScanResponse
|
import com.tangem.domain.models.scan.ScanResponse
|
||||||
import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent
|
import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent
|
||||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||||
|
|
@ -73,14 +72,8 @@ internal object UseCaseScanProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showScanFailsDialog(source: AnalyticsParam.ScreensSources) {
|
private fun showScanFailsDialog(source: AnalyticsParam.ScreensSources) {
|
||||||
val scanFailsSource = when (source) {
|
|
||||||
is AnalyticsParam.ScreensSources.SignIn -> ScanFailsRequester.Source.SIGN_IN
|
|
||||||
is AnalyticsParam.ScreensSources.Settings -> ScanFailsRequester.Source.SETTINGS
|
|
||||||
is AnalyticsParam.ScreensSources.Intro -> ScanFailsRequester.Source.INTRO
|
|
||||||
else -> ScanFailsRequester.Source.MAIN
|
|
||||||
}
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
store.inject(DaggerGraphState::scanFailsRequester).show(scanFailsSource)
|
store.inject(DaggerGraphState::scanFailsRequester).show(source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.tangem.core.analytics.models.AnalyticsParam
|
||||||
import com.tangem.core.decompose.context.AppComponentContext
|
import com.tangem.core.decompose.context.AppComponentContext
|
||||||
import com.tangem.core.decompose.factory.ComponentFactory
|
import com.tangem.core.decompose.factory.ComponentFactory
|
||||||
import com.tangem.core.decompose.model.getOrCreateModel
|
import com.tangem.core.decompose.model.getOrCreateModel
|
||||||
|
|
@ -21,7 +22,7 @@ internal class ScanFailsComponent @AssistedInject constructor(
|
||||||
|
|
||||||
private val model: ScanFailsModel = getOrCreateModel(params)
|
private val model: ScanFailsModel = getOrCreateModel(params)
|
||||||
|
|
||||||
override suspend fun show(source: ScanFailsRequester.Source): ScanFailsRequester.Result {
|
override suspend fun show(source: AnalyticsParam.ScreensSources): ScanFailsRequester.Result {
|
||||||
model.show(source)
|
model.show(source)
|
||||||
return model.waitResult()
|
return model.waitResult()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,14 +30,13 @@ internal class ScanFailsModel @Inject constructor(
|
||||||
val uiState: StateFlow<ScanFailsUM>
|
val uiState: StateFlow<ScanFailsUM>
|
||||||
field = MutableStateFlow(ScanFailsUM(onDismiss = ::dismiss))
|
field = MutableStateFlow(ScanFailsUM(onDismiss = ::dismiss))
|
||||||
|
|
||||||
fun show(source: ScanFailsRequester.Source) {
|
fun show(source: AnalyticsParam.ScreensSources) {
|
||||||
val analyticsSource = source.toAnalyticsSource()
|
|
||||||
result.value = null
|
result.value = null
|
||||||
uiState.update {
|
uiState.update {
|
||||||
ScanFailsUM(
|
ScanFailsUM(
|
||||||
isShown = true,
|
isShown = true,
|
||||||
onHowToScan = { onHowToScan(analyticsSource) },
|
onHowToScan = { onHowToScan(source) },
|
||||||
onRequestSupport = { onRequestSupport(analyticsSource) },
|
onRequestSupport = { onRequestSupport(source) },
|
||||||
onDismiss = ::dismiss,
|
onDismiss = ::dismiss,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -70,11 +69,4 @@ internal class ScanFailsModel @Inject constructor(
|
||||||
sendFeedbackEmailUseCase(type = FeedbackEmailType.ScanningProblem)
|
sendFeedbackEmailUseCase(type = FeedbackEmailType.ScanningProblem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ScanFailsRequester.Source.toAnalyticsSource(): AnalyticsParam.ScreensSources = when (this) {
|
|
||||||
ScanFailsRequester.Source.MAIN -> AnalyticsParam.ScreensSources.Main
|
|
||||||
ScanFailsRequester.Source.SIGN_IN -> AnalyticsParam.ScreensSources.SignIn
|
|
||||||
ScanFailsRequester.Source.SETTINGS -> AnalyticsParam.ScreensSources.Settings
|
|
||||||
ScanFailsRequester.Source.INTRO -> AnalyticsParam.ScreensSources.Intro
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.tangem.tap.features.scanfails
|
package com.tangem.tap.features.scanfails
|
||||||
|
|
||||||
|
import com.tangem.core.analytics.models.AnalyticsParam
|
||||||
import com.tangem.domain.card.ScanFailsRequester
|
import com.tangem.domain.card.ScanFailsRequester
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
|
@ -13,7 +14,7 @@ class ScanFailsRequesterProxy @Inject constructor() : ScanFailsRequester {
|
||||||
|
|
||||||
val componentRequester = MutableStateFlow<ScanFailsRequester?>(null)
|
val componentRequester = MutableStateFlow<ScanFailsRequester?>(null)
|
||||||
|
|
||||||
override suspend fun show(source: ScanFailsRequester.Source): ScanFailsRequester.Result {
|
override suspend fun show(source: AnalyticsParam.ScreensSources): ScanFailsRequester.Result {
|
||||||
return withTimeout(timeMillis = 1000) {
|
return withTimeout(timeMillis = 1000) {
|
||||||
componentRequester.filterNotNull().first()
|
componentRequester.filterNotNull().first()
|
||||||
}.show(source)
|
}.show(source)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ package com.tangem.tap.features.scanfails.di
|
||||||
|
|
||||||
import com.tangem.core.decompose.factory.ComponentFactory
|
import com.tangem.core.decompose.factory.ComponentFactory
|
||||||
import com.tangem.core.decompose.model.Model
|
import com.tangem.core.decompose.model.Model
|
||||||
|
import com.tangem.domain.card.ScanFailsCounter
|
||||||
import com.tangem.domain.card.ScanFailsRequester
|
import com.tangem.domain.card.ScanFailsRequester
|
||||||
|
import com.tangem.tap.domain.scanCard.DefaultScanFailsCounter
|
||||||
import com.tangem.tap.features.scanfails.ScanFailsComponent
|
import com.tangem.tap.features.scanfails.ScanFailsComponent
|
||||||
import com.tangem.tap.features.scanfails.ScanFailsModel
|
import com.tangem.tap.features.scanfails.ScanFailsModel
|
||||||
import com.tangem.tap.features.scanfails.ScanFailsRequesterProxy
|
import com.tangem.tap.features.scanfails.ScanFailsRequesterProxy
|
||||||
|
|
@ -29,4 +31,8 @@ internal interface ScanFailsModule {
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
fun bindRequester(impl: ScanFailsRequesterProxy): ScanFailsRequester
|
fun bindRequester(impl: ScanFailsRequesterProxy): ScanFailsRequester
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
fun bindScanFailsCounter(impl: DefaultScanFailsCounter): ScanFailsCounter
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
package com.tangem.tap.domain.scanCard
|
||||||
|
|
||||||
|
import com.tangem.core.analytics.models.AnalyticsParam
|
||||||
|
import com.tangem.domain.card.ScanFailsRequester
|
||||||
|
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||||
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
|
import io.mockk.clearMocks
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
internal class DefaultScanFailsCounterTest {
|
||||||
|
|
||||||
|
private val scanFailsRequester = mockk<ScanFailsRequester>()
|
||||||
|
private val dispatchers = TestingCoroutineDispatcherProvider()
|
||||||
|
private val appScope = object : AppCoroutineScope {
|
||||||
|
override val coroutineContext = dispatchers.main
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var counter: DefaultScanFailsCounter
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
clearMocks(scanFailsRequester)
|
||||||
|
counter = DefaultScanFailsCounter(
|
||||||
|
scanFailsRequester = scanFailsRequester,
|
||||||
|
appScope = appScope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `single user cancellation does not show dialog`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val source = AnalyticsParam.ScreensSources.Main
|
||||||
|
coEvery { scanFailsRequester.show(source) } returns ScanFailsRequester.Result.Dismissed
|
||||||
|
|
||||||
|
// Act
|
||||||
|
counter.onScanFailure(isUserCancelled = true, source = source)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
coVerify(exactly = 0) { scanFailsRequester.show(source) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `two consecutive user cancellations show dialog`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val source = AnalyticsParam.ScreensSources.Main
|
||||||
|
coEvery { scanFailsRequester.show(source) } returns ScanFailsRequester.Result.Dismissed
|
||||||
|
|
||||||
|
// Act
|
||||||
|
counter.onScanFailure(isUserCancelled = true, source = source)
|
||||||
|
counter.onScanFailure(isUserCancelled = true, source = source)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
coVerify(exactly = 1) { scanFailsRequester.show(source) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `non-cancelled failure resets counter`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val source = AnalyticsParam.ScreensSources.Main
|
||||||
|
coEvery { scanFailsRequester.show(source) } returns ScanFailsRequester.Result.Dismissed
|
||||||
|
|
||||||
|
// Act
|
||||||
|
counter.onScanFailure(isUserCancelled = true, source = source)
|
||||||
|
counter.onScanFailure(isUserCancelled = false, source = source)
|
||||||
|
counter.onScanFailure(isUserCancelled = true, source = source)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
coVerify(exactly = 0) { scanFailsRequester.show(source) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `reset clears counter`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val source = AnalyticsParam.ScreensSources.Main
|
||||||
|
coEvery { scanFailsRequester.show(source) } returns ScanFailsRequester.Result.Dismissed
|
||||||
|
|
||||||
|
// Act
|
||||||
|
counter.onScanFailure(isUserCancelled = true, source = source)
|
||||||
|
counter.reset()
|
||||||
|
counter.onScanFailure(isUserCancelled = true, source = source)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
coVerify(exactly = 0) { scanFailsRequester.show(source) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `after reset two new cancellations show dialog again`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val source = AnalyticsParam.ScreensSources.SignIn
|
||||||
|
coEvery { scanFailsRequester.show(source) } returns ScanFailsRequester.Result.Dismissed
|
||||||
|
|
||||||
|
// Act
|
||||||
|
counter.onScanFailure(isUserCancelled = true, source = source)
|
||||||
|
counter.onScanFailure(isUserCancelled = true, source = source)
|
||||||
|
counter.reset()
|
||||||
|
counter.onScanFailure(isUserCancelled = true, source = source)
|
||||||
|
counter.onScanFailure(isUserCancelled = true, source = source)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
coVerify(exactly = 2) { scanFailsRequester.show(source) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dialog receives correct source`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val source = AnalyticsParam.ScreensSources.Settings
|
||||||
|
coEvery { scanFailsRequester.show(source) } returns ScanFailsRequester.Result.Dismissed
|
||||||
|
|
||||||
|
// Act
|
||||||
|
counter.onScanFailure(isUserCancelled = true, source = source)
|
||||||
|
counter.onScanFailure(isUserCancelled = true, source = source)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
coVerify(exactly = 1) { scanFailsRequester.show(source) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `third consecutive cancellation also triggers dialog`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val source = AnalyticsParam.ScreensSources.Intro
|
||||||
|
coEvery { scanFailsRequester.show(source) } returns ScanFailsRequester.Result.Dismissed
|
||||||
|
|
||||||
|
// Act
|
||||||
|
counter.onScanFailure(isUserCancelled = true, source = source)
|
||||||
|
counter.onScanFailure(isUserCancelled = true, source = source)
|
||||||
|
counter.onScanFailure(isUserCancelled = true, source = source)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
coVerify(exactly = 2) { scanFailsRequester.show(source) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.tangem.domain.card
|
||||||
|
|
||||||
|
import com.tangem.core.analytics.models.AnalyticsParam
|
||||||
|
|
||||||
|
interface ScanFailsCounter {
|
||||||
|
|
||||||
|
fun reset()
|
||||||
|
|
||||||
|
fun onScanFailure(isUserCancelled: Boolean, source: AnalyticsParam.ScreensSources)
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package com.tangem.domain.card
|
package com.tangem.domain.card
|
||||||
|
|
||||||
|
import com.tangem.core.analytics.models.AnalyticsParam
|
||||||
|
|
||||||
interface ScanFailsRequester {
|
interface ScanFailsRequester {
|
||||||
|
|
||||||
suspend fun show(source: Source): Result
|
suspend fun show(source: AnalyticsParam.ScreensSources): Result
|
||||||
|
|
||||||
enum class Source { MAIN, SIGN_IN, SETTINGS, INTRO }
|
|
||||||
|
|
||||||
sealed class Result {
|
sealed class Result {
|
||||||
data object Dismissed : Result()
|
data object Dismissed : Result()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue