diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt index 1bac186d83..06df7c2274 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index 757e78cb34..cf1c9f95be 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt index 02e1701894..3a0902520a 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt @@ -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 { 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)) + } } } diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt index 8f72ce92f8..c7c56864ec 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt @@ -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 { +) : ResultChain() { - override suspend fun invoke( - previousChainResult: ScanResponse, - ): Either { + override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult { val interceptor = CardContextInterceptor(previousChainResult) val params = event.params.toMutableMap() interceptor.intercept(params) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt index 3db1f7a79e..66f708e28d 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt @@ -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, private val tapWalletManager: TapWalletManager, -) : Chain { +) : ResultChain() { - override suspend fun invoke( - previousChainResult: ScanResponse, - ): Either { + override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult { tapWalletManager.updateConfigManager(previousChainResult) store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(previousChainResult)) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt index ccf4f495bd..35ad2efbab 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt @@ -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, private val disclaimerWillShow: () -> Unit = {}, -) : Chain { +) : ResultChain() { - override suspend fun invoke( - previousChainResult: ScanResponse, - ): Either { + 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 { + private suspend fun showDisclaimer(disclaimer: Disclaimer, response: ScanResponse): ScanChainResult { store.dispatchOnMain(DisclaimerAction.SetDisclaimer(disclaimer)) return suspendCancellableCoroutine { continuation -> diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/FailedScansCounterChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/FailedScansCounterChain.kt new file mode 100644 index 0000000000..9698824c24 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/FailedScansCounterChain.kt @@ -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 { + + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt index 588dc809e2..ae5e9f8ad3 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt @@ -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] diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainResult.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainResult.kt new file mode 100644 index 0000000000..e83f4a1578 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainResult.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanningFinishedChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanningFinishedChain.kt deleted file mode 100644 index a8c116cbe8..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanningFinishedChain.kt +++ /dev/null @@ -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 { - - override suspend fun invoke(previousChainResult: ScanResponse): Either { - onScanningFinished() - return previousChainResult.right() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt index f74d706775..8aeacbb2c9 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt @@ -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 = 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) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt index e87ba1ee99..cb2d157825 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 2bec7b7af6..8b83c455cf 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -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)) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt index 81f1adee30..bc71e1fef2 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt @@ -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) } diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt index 7ffd5d139e..2e584d3d35 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt @@ -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 = initDefaultStories(), ) : StateType { diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt index 3f153ee689..667e29f062 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt @@ -1,14 +1,20 @@ package com.tangem.domain.card -// TODO: May be add new error types sealed class ScanCardException : Exception() { - object UserCancelled : ScanCardException() - object WrongAccessCode : ScanCardException() + data object UserCancelled : ScanCardException() { + @Suppress("UnusedPrivateMember") + private fun readResolve(): Any = UserCancelled + } + + data object WrongAccessCode : ScanCardException() { + @Suppress("UnusedPrivateMember") + private fun readResolve(): Any = WrongAccessCode + } open class ChainException : ScanCardException() - data class UnknownException(override val cause: Exception) : ScanCardException() + data class UnknownException(override val cause: Throwable) : ScanCardException() data class WrongCardId(val cardId: String) : ScanCardException() } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt index 961d9acd07..4dfc1c9668 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt @@ -16,7 +16,6 @@ interface ScanCardProcessor { analyticsEvent: AnalyticsEvent? = null, cardId: String? = null, onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {}, - onScanStateChange: suspend (scanInProgress: Boolean) -> Unit = {}, onWalletNotCreated: suspend () -> Unit = {}, disclaimerWillShow: () -> Unit = {}, onFailure: suspend (error: TangemError) -> Unit = {}, diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt index 69dbc7a617..04abfb1fce 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt @@ -1,9 +1,7 @@ package com.tangem.domain.card import arrow.core.Either -import arrow.core.EitherNel -import arrow.core.flatMap -import com.tangem.TangemSdk +import arrow.core.raise.either import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.ScanCardRepository import com.tangem.domain.core.chain.Chain @@ -11,12 +9,7 @@ import com.tangem.domain.core.chain.ChainProcessor import com.tangem.domain.models.scan.ScanResponse /** - * Use case responsible for scanning a card and returning a [ScanResponse] object - * - * @property scanCardRepository a repository object implementing [ScanCardRepository] interface - * @property cardSdkConfigRepository an instance of [TangemSdk] to configure the display format of the card ID - * - * @constructor create a new instance of [ScanCardUseCase] with the given dependencies + * Use case responsible for scanning a card and returning a [ScanResponse] object. */ class ScanCardUseCase( private val scanCardRepository: ScanCardRepository, @@ -24,37 +17,41 @@ class ScanCardUseCase( ) { /** A [ChainProcessor] object to launch the after-scan chains */ - private val scanChainProcessor by lazy { - ChainProcessor() - } + private val scanChainProcessor = ChainProcessor() /** - * Scan a card and return a [ScanResponse] object. + * Scan a card. * - * @param cardId an optional card ID to scan. If null, can scan any card present - * @param allowRequestAccessCodeFromStorage whether to prompt the user for an access code if needed - * @param afterScanChains a list of chains that should be executed after a successful card scan - * operation. Defaults to an empty array + * @param cardId An optional card ID to scan. If `null`, can scan any card present + * @param allowRequestAccessCodeFromStorage Whether to prompt the user for an access code if needed + * @param afterScanChains A list of chains that should be executed after a card scan + * operation. Defaults to an empty array. * - * @return a [EitherNel] object with either a non-empty list of [ScanCardException] or a [ScanResponse] + * @return [Either] object with either a [ScanCardException] or a [ScanResponse] object. */ suspend operator fun invoke( cardId: String? = null, allowRequestAccessCodeFromStorage: Boolean = false, - afterScanChains: List> = emptyList(), - ): Either { + afterScanChains: List> = emptyList(), + ): Either = either { cardSdkConfigRepository.resetCardIdDisplayFormat() - scanChainProcessor.addChains(afterScanChains) + scanChainProcessor.setChains(afterScanChains) - return scanCardRepository.scanCard( - cardId = cardId, - allowRequestAccessCodeFromStorage = allowRequestAccessCodeFromStorage, - ) - .onRight { scanResponse -> - cardSdkConfigRepository.updateCardIdDisplayFormat(scanResponse.productType) - } - .flatMap { response -> - scanChainProcessor.launchChains(initial = response) + val maybeScanResponse = scanCard(cardId, allowRequestAccessCodeFromStorage) + val scanResponse = scanChainProcessor.launchChains(maybeScanResponse).bind() + + cardSdkConfigRepository.updateCardIdDisplayFormat(scanResponse.productType) + + scanResponse + } + + private suspend fun scanCard( + cardId: String?, + allowRequestAccessCodeFromStorage: Boolean, + ): Either { + return Either.catch { scanCardRepository.scanCard(cardId, allowRequestAccessCodeFromStorage) } + .mapLeft { e -> + e as? ScanCardException ?: ScanCardException.UnknownException(e) } } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt index 2de00090cf..90f2016383 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt @@ -1,6 +1,5 @@ package com.tangem.domain.card.repository -import arrow.core.Either import com.tangem.domain.card.ScanCardException import com.tangem.domain.models.scan.ScanResponse @@ -15,10 +14,8 @@ interface ScanCardRepository { * @param cardId an optional card ID to scan. If null, the repository should scan any card present. * @param allowRequestAccessCodeFromStorage whether the access code can be requested from * internal storage during the scan process - * @return an [Either] that contains a [ScanCardException] in case of an error or a [ScanResponse]. + * @return a [ScanResponse] object with the result of the scan. + * @throws [ScanCardException] if the scan process fails. */ - suspend fun scanCard( - cardId: String?, - allowRequestAccessCodeFromStorage: Boolean, - ): Either + suspend fun scanCard(cardId: String?, allowRequestAccessCodeFromStorage: Boolean): ScanResponse } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chain.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chain.kt deleted file mode 100644 index a7e0d5bc38..0000000000 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chain.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.domain.core.chain - -import arrow.core.Either - -/** - * A chain in the [ChainProcessor] class for processing a chain of operations with the ability to handle errors. - * @param E the type of error - * @param R the type of result - */ -interface Chain { - - /** - * Invokes the chain with the previous chain result as input and returns an [Either] result. - * @param previousChainResult the previous chain result as an [R] - * @return the result of the chain processing as an [Either] - */ - suspend operator fun invoke(previousChainResult: R): Either -} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt index c1cac09eb3..4e95318b6c 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt @@ -1,7 +1,6 @@ package com.tangem.domain.core.chain import arrow.core.Either -import arrow.core.raise.either // TODO: Create Ior implementation // TODO: Create accumulate implementation @@ -10,7 +9,7 @@ import arrow.core.raise.either * @param E the type of error * @param R the type of result */ -class ChainProcessor { +class ChainProcessor { /** * The list of chains to be executed in order. @@ -18,18 +17,20 @@ class ChainProcessor { private val chains: MutableList> = mutableListOf() /** - * Adds chains to the existing list of chains to be executed. - * @param chains the chains to be added to the list + * Sets the list of chains to be executed. + * @param chains the chains to be set */ - fun addChains(chains: List>) { + fun setChains(chains: List>) { + this.chains.clear() this.chains.addAll(chains) } - suspend fun launchChains(initial: R): Either { - return either { - chains.fold(initial) { prevChainResult, chain -> - chain.invoke(prevChainResult).bind() - } + /** + * Launches the chains with the [initial] value. + * */ + suspend fun launchChains(initial: Either): Either { + return chains.fold(initial) { prevChainResult, chain -> + chain.launch(prevChainResult) } } } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chains.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chains.kt new file mode 100644 index 0000000000..02b5da3924 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chains.kt @@ -0,0 +1,44 @@ +package com.tangem.domain.core.chain + +import arrow.core.Either +import arrow.core.flatMap + +/** + * A chain in the [ChainProcessor] class for processing a chain of operations with the ability to handle errors. + * @param E the type of error + * @param R the type of result + */ +interface Chain { + + /** + * Invokes the chain with the previous chain result as input and returns an [Either] result. + * + * @param previousChainResult the previous chain result as an [Either] + * @return the result of the chain processing as an [Either] + */ + suspend fun launch(previousChainResult: Either): Either +} + +/** + * A chain in the [ChainProcessor] class for processing a chain of operations with the ability to handle errors. + * + * This chain will be launched only if the previous chains in the [ChainProcessor] have + * returned a right side of [Either]. + * + * @param E the type of error + * @param R the type of result + */ +abstract class ResultChain : Chain { + + final override suspend fun launch(previousChainResult: Either): Either { + return previousChainResult.flatMap { launch(it) } + } + + /** + * Invokes the chain with the previous chain result as input and returns a result. + * + * @param previousChainResult the previous chain result + * @return the result of the chain processing as an [Either] + */ + abstract suspend fun launch(previousChainResult: R): Either +} \ No newline at end of file