diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 89ca7c3734..2d3445cf24 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,6 +13,8 @@ dependencies { implementation(files("libs/walletconnect-1.5.6.aar")) implementation(project(":domain:legacy")) implementation(project(":domain:models")) + implementation(project(":domain:core")) + implementation(project(":domain:card")) implementation(project(":common")) implementation(project(":core:analytics")) implementation(project(":core:featuretoggles")) 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 new file mode 100644 index 0000000000..febc6a1525 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt @@ -0,0 +1,36 @@ +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.AnalyticsEvent +import com.tangem.domain.card.ScanCardException +import com.tangem.domain.core.chain.Chain +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor + +/** + * Handles the analytics event interception and sending event after the scan card operation. Always returns result of + * previous chain. + * + * @param event the analytics event to be handled by this chain. + * + * @see Chain for more information about the Chain interface. + */ +class AnalyticsChain( + private val event: AnalyticsEvent, +) : Chain { + + override suspend fun invoke( + previousChainResult: ScanResponse, + ): Either { + val interceptor = CardContextInterceptor(previousChainResult) + val params = event.params.toMutableMap() + interceptor.intercept(params) + event.params = params.toMap() + + Analytics.send(event) + + return previousChainResult.right() + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..21d0e75290 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt @@ -0,0 +1,126 @@ +package com.tangem.tap.domain.scanCard.chains + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.common.services.Result +import com.tangem.core.analytics.Analytics +import com.tangem.data.source.preferences.PreferencesDataSource +import com.tangem.domain.card.ScanCardException +import com.tangem.domain.common.extensions.withMainContext +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.twinsIsTwinned +import com.tangem.domain.core.chain.Chain +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE +import com.tangem.tap.common.extensions.addContext +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.setContext +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.common.redux.navigation.AppScreen +import com.tangem.tap.domain.TapWalletManager +import com.tangem.tap.features.onboarding.OnboardingHelper +import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper +import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction +import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManagerFactory +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler +import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction +import kotlinx.coroutines.delay +import org.rekotlin.Store + +/** + * Handles the verification process to determine if the scanned card requires onboarding. + * + * Returns: + * - [ScanChainException.OnboardingNeeded] if onboarding required for none SaltPay cards. + * - [ScanChainException.CheckForSaltPayOnboardingCaseException] if unable to check for SaltPay onboarding case. + * - [ScanChainException.PutSaltPayVisaCard] if scanned SaltPay card is not activated and onboarding required. + * + * @param store the [Store] that holds the state of the app. + * @param tapWalletManager manager responsible for handling operations related to the Wallet. + * @param preferencesStore data source for user preferences. + * + * @see Chain for more information about the Chain interface. + */ +class CheckForOnboardingChain( + private val store: Store, + private val tapWalletManager: TapWalletManager, + private val preferencesStore: PreferencesDataSource, +) : Chain { + + override suspend fun invoke( + previousChainResult: ScanResponse, + ): Either { + val cardTypesResolver = previousChainResult.cardTypesResolver + + tapWalletManager.updateConfigManager(previousChainResult) + store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(previousChainResult)) + + return when { + cardTypesResolver.isSaltPayVisa() -> { + val manager = SaltPayActivationManagerFactory( + blockchain = previousChainResult.cardTypesResolver.getBlockchain(), + card = previousChainResult.card, + ).create() + val result = OnboardingSaltPayHelper.isOnboardingCase(previousChainResult, manager) + delay(timeMillis = 500) + withMainContext { + when (result) { + is Result.Success -> { + Analytics.addContext(previousChainResult) + val isOnboardingCase = result.data + if (isOnboardingCase) { + store.dispatch( + GlobalAction.Onboarding.Start(previousChainResult, canSkipBackup = false), + ) + store.dispatch(OnboardingSaltPayAction.SetDependencies(manager)) + store.dispatch(OnboardingSaltPayAction.Update(withAnalytics = false)) + ScanChainException.OnboardingNeeded(AppScreen.OnboardingWallet).left() + } else { + delay(DELAY_SDK_DIALOG_CLOSE) + previousChainResult.right() + } + } + is Result.Failure -> { + delay(DELAY_SDK_DIALOG_CLOSE) + SaltPayExceptionHandler.handle(result.error) + ScanChainException.CheckForSaltPayOnboardingCaseException(result.error).left() + } + } + } + } + cardTypesResolver.isSaltPayWallet() -> { + delay(DELAY_SDK_DIALOG_CLOSE) + if (previousChainResult.card.backupStatus?.isActive == false) { + SaltPayExceptionHandler.handle(SaltPayActivationError.PutVisaCard) + ScanChainException.PutSaltPayVisaCard.left() + } else { + Analytics.setContext(previousChainResult) + previousChainResult.right() + } + } + OnboardingHelper.isOnboardingCase(previousChainResult) -> { + Analytics.addContext(previousChainResult) + store.dispatchOnMain(GlobalAction.Onboarding.Start(previousChainResult, canSkipBackup = true)) + val appScreen = OnboardingHelper.whereToNavigate(previousChainResult) + ScanChainException.OnboardingNeeded(appScreen).left() + } + else -> { + Analytics.setContext(previousChainResult) + // If twins was twinned previously but twins welcome not shown + if (previousChainResult.twinsIsTwinned() && !preferencesStore.wasTwinsOnboardingShown()) { + store.dispatchOnMain( + TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)), + ) + ScanChainException.OnboardingNeeded(AppScreen.OnboardingTwins).left() + } else { + delay(DELAY_SDK_DIALOG_CLOSE) + previousChainResult.right() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForUnfinishedSaltPayBackupChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForUnfinishedSaltPayBackupChain.kt new file mode 100644 index 0000000000..f01f9f76d7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForUnfinishedSaltPayBackupChain.kt @@ -0,0 +1,44 @@ +package com.tangem.tap.domain.scanCard.chains + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.card.ScanCardException +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.core.chain.Chain +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.operations.backup.BackupService +import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa + +/** + * Handles the verification for unfinished SaltPay backup processes after the card scanning operation. + * + * Returns [ScanChainException.PutSaltPayVisaCard] if backup is not finished. + * + * @param backupService a service responsible for managing backup operations, used here to check for unfinished backups + * and verify the primary card type. + * + * @see Chain for more information about the Chain interface. + */ +internal class CheckForUnfinishedSaltPayBackupChain( + private val backupService: BackupService, +) : Chain { + + override suspend fun invoke( + previousChainResult: ScanResponse, + ): Either { + if (!backupService.hasIncompletedBackup || !backupService.primaryCardIsSaltPayVisa()) { + return previousChainResult.right() + } + + val isTheSamePrimaryCard = backupService.primaryCardId + ?.let { it == previousChainResult.card.cardId } + ?: false + + return if (previousChainResult.cardTypesResolver.isSaltPayWallet() || !isTheSamePrimaryCard) { + ScanChainException.PutSaltPayVisaCard.left() + } else { + previousChainResult.right() + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..b1addc1718 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt @@ -0,0 +1,75 @@ +package com.tangem.tap.domain.scanCard.chains + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.card.ScanCardException +import com.tangem.domain.core.chain.Chain +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.navigation.AppScreen +import com.tangem.tap.features.disclaimer.Disclaimer +import com.tangem.tap.features.disclaimer.createDisclaimer +import com.tangem.tap.features.disclaimer.redux.DisclaimerAction +import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback +import kotlinx.coroutines.suspendCancellableCoroutine +import org.rekotlin.Store +import kotlin.coroutines.resume + +/** + * Handles disclaimer display after the card scanning operation. + * + * Returns [ScanChainException.DisclaimerWasCanceled] if the disclaimer is dismissed by the user. + * + * @param store the [Store] that holds the state of the app, used here to dispatch actions related to disclaimers. + * @param disclaimerWillShow an optional function to be invoked when a disclaimer is about to be shown. Default is an + * empty function. + * + * @see Chain for more information about the Chain interface. + */ +internal class DisclaimerChain( + private val store: Store, + private val disclaimerWillShow: () -> Unit = {}, +) : Chain { + + override suspend fun invoke( + previousChainResult: ScanResponse, + ): Either { + val disclaimer = previousChainResult.card.createDisclaimer() + + return if (disclaimer.isAccepted()) { + previousChainResult.right() + } else { + disclaimerWillShow() + showDisclaimer(disclaimer, previousChainResult) + } + } + + private suspend fun showDisclaimer( + disclaimer: Disclaimer, + response: ScanResponse, + ): Either { + store.dispatchOnMain(DisclaimerAction.SetDisclaimer(disclaimer)) + + return suspendCancellableCoroutine { continuation -> + store.dispatchOnMain( + DisclaimerAction.Show( + fromScreen = AppScreen.Home, + callback = DisclaimerCallback( + onAccept = { + if (continuation.isActive) { + continuation.resume(response.right()) + } + }, + onDismiss = { + if (continuation.isActive) { + continuation.resume(ScanChainException.DisclaimerWasCanceled.left()) + } + }, + ), + ), + ) + } + } +} \ 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 new file mode 100644 index 0000000000..626465c7ee --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt @@ -0,0 +1,31 @@ +package com.tangem.tap.domain.scanCard.chains + +import com.tangem.domain.card.ScanCardException +import com.tangem.tap.common.redux.navigation.AppScreen + +sealed class ScanChainException : ScanCardException.ChainException() { + + /** + * May be returned from [CheckForOnboardingChain] and [CheckForUnfinishedSaltPayBackupChain] + * */ + object PutSaltPayVisaCard : ScanChainException() + + /** + * May be returned from [DisclaimerChain] + * */ + object DisclaimerWasCanceled : ScanChainException() + + /** + * May be returned from [CheckForOnboardingChain] + * + * @param onboardingRoute route where to navigate + * */ + data class OnboardingNeeded(val onboardingRoute: AppScreen) : ScanChainException() + + /** + * May be returned from [CheckForOnboardingChain] + * + * @param cause cause of exception + * */ + data class CheckForSaltPayOnboardingCaseException(override val cause: Throwable?) : ScanChainException() +} \ 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 new file mode 100644 index 0000000000..a8c116cbe8 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanningFinishedChain.kt @@ -0,0 +1,25 @@ +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