Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-07 12:31:42 +03:00
parent db178b206b
commit b8c6c0471a
22 changed files with 191 additions and 165 deletions

View file

@ -30,7 +30,6 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
analyticsEvent: AnalyticsEvent?,
cardId: String?,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
onScanStateChange: suspend (scanInProgress: Boolean) -> Unit,
onWalletNotCreated: suspend () -> Unit,
disclaimerWillShow: () -> Unit,
onFailure: suspend (error: TangemError) -> Unit,
@ -41,7 +40,6 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
analyticsEvent,
cardId,
onProgressStateChange,
onScanStateChange,
onWalletNotCreated,
disclaimerWillShow,
onFailure,
@ -52,7 +50,6 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
analyticsEvent,
cardId,
onProgressStateChange,
onScanStateChange,
onWalletNotCreated,
disclaimerWillShow,
onFailure,

View file

@ -48,14 +48,12 @@ internal object LegacyScanProcessor {
analyticsEvent: AnalyticsEvent?,
cardId: String?,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
onScanStateChange: suspend (scanInProgress: Boolean) -> Unit,
onWalletNotCreated: suspend () -> Unit,
disclaimerWillShow: () -> Unit,
onFailure: suspend (error: TangemError) -> Unit,
onSuccess: suspend (scanResponse: ScanResponse) -> Unit,
) = withMainContext {
onProgressStateChange(true)
onScanStateChange(true)
tangemSdkManager.changeDisplayedCardIdNumbersCount(null)
@ -65,13 +63,11 @@ internal object LegacyScanProcessor {
result
.doOnFailure { error ->
onScanStateChange(false)
onFailure(error)
}
.doOnSuccess { scanResponse ->
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
onScanStateChange(false)
sendAnalytics(analyticsEvent, scanResponse)
showDisclaimerIfNeed(

View file

@ -3,11 +3,15 @@ package com.tangem.tap.domain.scanCard
import arrow.fx.coroutines.resourceScope
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.StateDialog
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.scanCard.chains.*
@ -25,9 +29,15 @@ internal object UseCaseScanProcessor {
allowsRequestAccessCodeFromRepository: Boolean = false,
): CompletionResult<ScanResponse> {
val scanCardUseCase = store.inject(DaggerGraphState::scanCardUseCase)
return scanCardUseCase(cardId, allowsRequestAccessCodeFromRepository)
.fold(
ifLeft = { CompletionResult.Failure(scanCardExceptionConverter.convertBack(it)) },
ifLeft = {
val error = scanCardExceptionConverter.convertBack(it)
Analytics.send(Basic.ScanError(error))
CompletionResult.Failure(error)
},
ifRight = { CompletionResult.Success(it) },
)
}
@ -37,17 +47,14 @@ internal object UseCaseScanProcessor {
analyticsEvent: AnalyticsEvent?,
cardId: String?,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
onScanStateChange: suspend (scanInProgress: Boolean) -> Unit,
onWalletNotCreated: suspend () -> Unit,
disclaimerWillShow: () -> Unit,
onFailure: suspend (error: TangemError) -> Unit,
onSuccess: suspend (scanResponse: ScanResponse) -> Unit,
) = progressScope(onProgressStateChange) {
onScanStateChange(true)
val scanCardUseCase = store.inject(DaggerGraphState::scanCardUseCase)
val chains = buildList {
add(ScanningFinishedChain { onScanStateChange(false) })
add(FailedScansCounterChain(UseCaseScanProcessor::showMaxUnsuccessfulScansReachedDialog))
if (analyticsEvent != null) {
add(AnalyticsChain(analyticsEvent))
}
@ -55,9 +62,14 @@ internal object UseCaseScanProcessor {
add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager))
}
scanCardUseCase(cardId, afterScanChains = chains)
.map { onSuccess(it) }
.mapLeft { proceedWithException(it, onWalletNotCreated, onFailure) }
scanCardUseCase(cardId, afterScanChains = chains).fold(
ifLeft = { proceedWithException(it, onWalletNotCreated, onFailure) },
ifRight = { onSuccess(it) },
)
}
private fun showMaxUnsuccessfulScansReachedDialog() {
store.dispatchDialogShow(StateDialog.ScanFailsDialog)
}
private suspend fun proceedWithException(
@ -75,7 +87,12 @@ internal object UseCaseScanProcessor {
is ScanCardException.UserCancelled,
is ScanCardException.WrongAccessCode,
is ScanCardException.WrongCardId,
-> onFailure(scanCardExceptionConverter.convertBack(exception))
-> {
val error = scanCardExceptionConverter.convertBack(exception)
Analytics.send(Basic.ScanError(error))
onFailure(error)
}
}
}
@ -89,8 +106,9 @@ internal object UseCaseScanProcessor {
navigateTo(exception.onboardingRoute)
onWalletNotCreated()
}
is ScanChainException.DisclaimerWasCanceled,
-> onFailure(scanCardExceptionConverter.convertBack(exception))
is ScanChainException.DisclaimerWasCanceled -> {
onFailure(scanCardExceptionConverter.convertBack(exception))
}
}
}

View file

@ -1,11 +1,11 @@
package com.tangem.tap.domain.scanCard.chains
import arrow.core.Either
import arrow.core.right
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.core.chain.ResultChain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
@ -19,11 +19,9 @@ import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
*/
class AnalyticsChain(
private val event: AnalyticsEvent,
) : Chain<ScanCardException.ChainException, ScanResponse> {
) : ResultChain<ScanCardException, ScanResponse>() {
override suspend fun invoke(
previousChainResult: ScanResponse,
): Either<ScanCardException.ChainException, ScanResponse> {
override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult {
val interceptor = CardContextInterceptor(previousChainResult)
val params = event.params.toMutableMap()
interceptor.intercept(params)

View file

@ -1,6 +1,5 @@
package com.tangem.tap.domain.scanCard.chains
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.core.analytics.Analytics
@ -9,6 +8,7 @@ import com.tangem.domain.card.ScanCardException
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.core.chain.ResultChain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchOnMain
@ -39,11 +39,9 @@ import org.rekotlin.Store
class CheckForOnboardingChain(
private val store: Store<AppState>,
private val tapWalletManager: TapWalletManager,
) : Chain<ScanCardException.ChainException, ScanResponse> {
) : ResultChain<ScanCardException, ScanResponse>() {
override suspend fun invoke(
previousChainResult: ScanResponse,
): Either<ScanCardException.ChainException, ScanResponse> {
override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult {
tapWalletManager.updateConfigManager(previousChainResult)
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(previousChainResult))

View file

@ -1,11 +1,11 @@
package com.tangem.tap.domain.scanCard.chains
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.core.navigation.AppScreen
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.core.chain.ResultChain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
@ -31,11 +31,9 @@ import kotlin.coroutines.resume
internal class DisclaimerChain(
private val store: Store<AppState>,
private val disclaimerWillShow: () -> Unit = {},
) : Chain<ScanCardException.ChainException, ScanResponse> {
) : ResultChain<ScanCardException, ScanResponse>() {
override suspend fun invoke(
previousChainResult: ScanResponse,
): Either<ScanCardException.ChainException, ScanResponse> {
override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult {
val disclaimer = previousChainResult.card.createDisclaimer()
return if (disclaimer.isAccepted()) {
@ -46,10 +44,7 @@ internal class DisclaimerChain(
}
}
private suspend fun showDisclaimer(
disclaimer: Disclaimer,
response: ScanResponse,
): Either<ScanCardException.ChainException, ScanResponse> {
private suspend fun showDisclaimer(disclaimer: Disclaimer, response: ScanResponse): ScanChainResult {
store.dispatchOnMain(DisclaimerAction.SetDisclaimer(disclaimer))
return suspendCancellableCoroutine { continuation ->

View file

@ -0,0 +1,29 @@
package com.tangem.tap.domain.scanCard.chains
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.models.scan.ScanResponse
internal class FailedScansCounterChain(
private val onMaxUnsuccessfulScansReached: () -> Unit,
private val maxUnsuccessfulScans: Int = 3,
) : Chain<ScanCardException, ScanResponse> {
override suspend fun launch(previousChainResult: ScanChainResult): ScanChainResult {
if (previousChainResult.isLeft()) {
unsuccessfulScansCounter = unsuccessfulScansCounter.inc().coerceAtMost(maxUnsuccessfulScans)
if (unsuccessfulScansCounter == maxUnsuccessfulScans) {
onMaxUnsuccessfulScansReached()
}
} else {
unsuccessfulScansCounter = 0
}
return previousChainResult
}
private companion object {
var unsuccessfulScansCounter = 0
}
}

View file

@ -8,7 +8,11 @@ sealed class ScanChainException : ScanCardException.ChainException() {
/**
* May be returned from [DisclaimerChain]
* */
object DisclaimerWasCanceled : ScanChainException()
data object DisclaimerWasCanceled : ScanChainException() {
@Suppress("UnusedPrivateMember")
private fun readResolve(): Any = DisclaimerWasCanceled
}
/**
* May be returned from [CheckForOnboardingChain]

View file

@ -0,0 +1,7 @@
package com.tangem.tap.domain.scanCard.chains
import arrow.core.Either
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.models.scan.ScanResponse
internal typealias ScanChainResult = Either<ScanCardException, ScanResponse>

View file

@ -1,25 +0,0 @@
package com.tangem.tap.domain.scanCard.chains
import arrow.core.Either
import arrow.core.right
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.models.scan.ScanResponse
/**
* Responsible for invoking the callback at the end of the card scanning operation. Should be passed as last chain in
* after card scanning chains. Always returns result of previous chain.
*
* @param onScanningFinished a suspending function to be called when the scanning process has finished.
*
* @see Chain for more information about the Chain interface.
*/
internal class ScanningFinishedChain(
private val onScanningFinished: suspend () -> Unit,
) : Chain<ScanCardException.ChainException, ScanResponse> {
override suspend fun invoke(previousChainResult: ScanResponse): Either<ScanChainException, ScanResponse> {
onScanningFinished()
return previousChainResult.right()
}
}

View file

@ -1,32 +1,27 @@
package com.tangem.tap.domain.scanCard.repository
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.common.CompletionResult
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.card.repository.ScanCardRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter
// TODO: Move to the :data:card module
internal class DefaultScanCardRepository(
private val tangemSdkManager: TangemSdkManager,
) : ScanCardRepository {
private val exceptionConverter = ScanCardExceptionConverter()
override suspend fun scanCard(
cardId: String?,
allowRequestAccessCodeFromStorage: Boolean,
): Either<ScanCardException, ScanResponse> = either {
when (
override suspend fun scanCard(cardId: String?, allowRequestAccessCodeFromStorage: Boolean): ScanResponse {
return when (
val result = tangemSdkManager.scanProduct(
cardId = cardId,
allowsRequestAccessCodeFromRepository = allowRequestAccessCodeFromStorage,
)
) {
is CompletionResult.Success -> result.data
is CompletionResult.Failure -> raise(exceptionConverter.convert(result.error))
is CompletionResult.Failure -> throw exceptionConverter.convert(result.error)
}
}
}

View file

@ -3,7 +3,6 @@ package com.tangem.tap.features.home.redux
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.tap.common.entities.IndeterminateProgressButton
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
@ -29,6 +28,4 @@ sealed class HomeAction : Action {
data class GoToShop(val userCountryCode: String?) : HomeAction()
data class UpdateCountryCode(val userCountryCode: String) : HomeAction()
data class ChangeScanCardButtonState(val state: IndeterminateProgressButton) : HomeAction()
}

View file

@ -13,12 +13,10 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.features.signin.redux.SignInAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -31,6 +29,8 @@ import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
private const val HIDE_PROGRESS_DELAY = 400L
object HomeMiddleware {
val handler = homeMiddleware
@ -84,17 +84,16 @@ private suspend fun readCard(analyticsEvent: AnalyticsEvent?) {
analyticsEvent = analyticsEvent,
onProgressStateChange = { showProgress ->
if (showProgress) {
changeButtonState(ButtonState.PROGRESS)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = true))
} else {
changeButtonState(ButtonState.ENABLED)
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
}
},
onScanStateChange = { scanInProgress ->
store.dispatch(HomeAction.ScanInProgress(scanInProgress))
},
onFailure = {
Timber.e(it, "Unable to scan card")
changeButtonState(ButtonState.ENABLED)
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
},
onSuccess = { scanResponse ->
proceedWithScanResponse(scanResponse)
@ -113,7 +112,7 @@ private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch {
Timber.e(error, "Unable to save user wallet")
}
.doOnSuccess {
scope.launch { store.onUserWalletSelected(userWallet = userWallet) }
scope.launch { store.onUserWalletSelected(userWallet) }
}
.doOnResult {
store.dispatchOnMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Card))
@ -123,10 +122,6 @@ private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch {
private suspend fun navigateTo(appScreen: AppScreen) {
store.dispatchOnMain(NavigationAction.NavigateTo(appScreen))
delay(timeMillis = 200)
changeButtonState(ButtonState.ENABLED)
}
private fun changeButtonState(state: ButtonState) {
store.dispatchOnMain(HomeAction.ChangeScanCardButtonState(IndeterminateProgressButton(state)))
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
}

View file

@ -22,9 +22,6 @@ private fun internalReduce(action: Action, appState: AppState): HomeState {
is HomeAction.ScanInProgress -> {
state = state.copy(scanInProgress = action.scanInProgress)
}
is HomeAction.ChangeScanCardButtonState -> {
state = state.copy(btnScanState = action.state)
}
is HomeAction.UpdateCountryCode -> {
state.onCountryCodeUpdate(state, action.userCountryCode)
}

View file

@ -1,15 +1,14 @@
package com.tangem.tap.features.home.redux
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.features.send.redux.states.ButtonState
import org.rekotlin.StateType
import java.util.Locale
@Immutable
data class HomeState(
val scanInProgress: Boolean = false,
val btnScanState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.ENABLED),
val stories: List<Stories> = initDefaultStories(),
) : StateType {