diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 05e9eed54d..f89ccbeb0a 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -18,6 +18,7 @@ import com.tangem.tap.common.DialogManager import com.tangem.tap.common.IntentHandler import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.SnackbarHandler +import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.NotificationsHandler import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction @@ -25,6 +26,8 @@ import com.tangem.tap.common.shop.GooglePayService import com.tangem.tap.common.shop.GooglePayService.Companion.LOAD_PAYMENT_DATA_REQUEST_CODE import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.userWalletList.UserWalletsListManager +import com.tangem.tap.domain.userWalletList.di.provideDummyImplementation import com.tangem.tap.features.shop.redux.ShopAction import com.tangem.tap.proxy.AppStateHolder import com.tangem.wallet.R @@ -40,6 +43,7 @@ import kotlin.coroutines.CoroutineContext lateinit var tangemSdk: TangemSdk lateinit var tangemSdkManager: TangemSdkManager lateinit var backupService: BackupService +lateinit var userWalletsListManager: UserWalletsListManager var notificationsHandler: NotificationsHandler? = null private val coroutineContext: CoroutineContext @@ -73,6 +77,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac tangemSdkManager = TangemSdkManager(tangemSdk, this) appStateHolder.tangemSdkManager = tangemSdkManager backupService = BackupService.init(tangemSdk, this) + userWalletsListManager = UserWalletsListManager.provideDummyImplementation() store.dispatch( ShopAction.CheckIfGooglePayAvailable( @@ -82,7 +87,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } private fun systemActions() { - WindowCompat.setDecorFitsSystemWindows(window, false) val windowInsetsController = WindowInsetsControllerCompat(window, binding.root) @@ -106,7 +110,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac val isOnboardingServiceActive = store.state.globalState.onboardingState.onboardingStarted val shopOpened = store.state.shopState.total != null if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore && !shopOpened)) { - store.dispatch(NavigationAction.NavigateTo(AppScreen.Home)) + val navigateTo = if (userWalletsListManager.hasSavedUserWallets) AppScreen.Welcome else AppScreen.Home + + store.dispatchOnMain(NavigationAction.NavigateTo(navigateTo)) } intentHandler.handleIntent(intent) } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt index d88560669e..1919cd3dbf 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -6,6 +6,7 @@ import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.TapError +import com.tangem.tap.domain.model.UserWallet import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.Dispatchers @@ -26,6 +27,11 @@ fun Store<*>.dispatchNotification(resId: Int) { dispatchOnMain(GlobalAction.ShowNotification(resId)) } +@Suppress("unused") // receiver type +fun Store<*>.onUserWalletSelected(userWallet: UserWallet, refresh: Boolean = false) { + // TODO: Load tokens for selected user wallet. Will be created in further MRs +} + fun Store<*>.dispatchToastNotification(resId: Int) { dispatchOnMain(GlobalAction.ShowToastNotification(resId)) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index a14cdb9925..6e55404733 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -15,6 +15,7 @@ import com.tangem.tap.features.shop.redux.ShopReducer import com.tangem.tap.features.tokens.redux.TokensReducer import com.tangem.tap.features.wallet.redux.reducers.WalletReducer import com.tangem.tap.proxy.AppStateHolder +import com.tangem.tap.features.welcome.redux.WelcomeReducer import org.rekotlin.Action fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder): AppState { @@ -36,6 +37,7 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder) tokensState = TokensReducer.reduce(action, state), walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState), shopState = ShopReducer.reduce(action, state.shopState), + welcomeState = WelcomeReducer.reduce(action, state), ) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index a158f2c872..b8b4bf689a 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -34,8 +34,9 @@ import com.tangem.tap.features.tokens.redux.TokensMiddleware import com.tangem.tap.features.tokens.redux.TokensState import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.middlewares.WalletMiddleware -import com.tangem.tap.features.welcome.redux.WelcomeState import com.tangem.tap.features.walletSelector.redux.WalletSelectorState +import com.tangem.tap.features.welcome.redux.WelcomeMiddleware +import com.tangem.tap.features.welcome.redux.WelcomeState import org.rekotlin.Middleware import org.rekotlin.StateType @@ -84,6 +85,7 @@ data class AppState( WalletConnectMiddleware().walletConnectMiddleware, BackupMiddleware().backupMiddleware, ShopMiddleware().shopMiddleware, + WelcomeMiddleware().middleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt index 8a2f5c5ac1..97d4c27134 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt @@ -2,13 +2,15 @@ package com.tangem.tap.common.redux.navigation import android.content.Intent import com.tangem.tap.common.CustomTabsManager +import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.openFragment import com.tangem.tap.common.extensions.popBackTo import com.tangem.tap.common.extensions.shareText import com.tangem.tap.common.redux.AppState +import com.tangem.tap.store import org.rekotlin.Middleware -val navigationMiddleware: Middleware = { dispatch, state -> +val navigationMiddleware: Middleware = { _, state -> { next -> { action -> if (action is NavigationAction) { @@ -16,16 +18,24 @@ val navigationMiddleware: Middleware = { dispatch, state -> when (action) { is NavigationAction.NavigateTo -> { navState?.activity?.get()?.openFragment( - action.screen, - action.addToBackstack, - action.fragmentShareTransition + screen = action.screen, + addToBackstack = action.addToBackstack, + fgShareTransition = action.fragmentShareTransition, ) } is NavigationAction.PopBackTo -> { - if (action.screen == AppScreen.Home) { - navState?.activity?.get()?.popBackTo(null, true) - } else { - navState?.activity?.get()?.popBackTo(action.screen) + when (val screen = action.screen) { + AppScreen.Home, + AppScreen.Welcome, + -> { + navState?.activity?.get()?.popBackTo(screen = null, inclusive = true) + if (navState?.backStack?.contains(screen) != true) { + store.dispatchOnMain(NavigationAction.NavigateTo(screen)) + } + } + else -> { + navState?.activity?.get()?.popBackTo(screen = action.screen) + } } } is NavigationAction.OpenUrl -> { @@ -39,10 +49,11 @@ val navigationMiddleware: Middleware = { dispatch, state -> navState?.activity?.get()?.startActivity(intent) } is NavigationAction.Share -> { - navState?.activity?.get()?.let { - it.shareText(action.data) - } + navState?.activity?.get()?.shareText(action.data) } + is NavigationAction.ActivityCreated, + is NavigationAction.ActivityDestroyed, + -> Unit } } next(action) diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt index e3d82a292a..cf60dc79f6 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt @@ -5,7 +5,7 @@ import org.rekotlin.StateType import java.lang.ref.WeakReference data class NavigationState( - val backStack: List = listOf(AppScreen.Home), + val backStack: List = emptyList(), val activity: WeakReference? = null, ) : StateType diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 66b16d1cad..b1f24b354d 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -8,11 +8,13 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.CardFilter import com.tangem.common.CompletionResult import com.tangem.common.SuccessResponse +import com.tangem.common.UserCodeType import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.CardIdDisplayFormat import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.Config import com.tangem.common.core.TangemSdkError +import com.tangem.common.core.UserCodeRequestPolicy import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.hdWallet.DerivationPath import com.tangem.common.map @@ -46,7 +48,10 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co userTokensRepository: UserTokensRepository, additionalBlockchainsToDerive: Collection? = null, messageRes: Int? = null, + useBiometricsForAccessCode: Boolean = false, ): CompletionResult { + setAccessCodeRequestPolicy(useBiometricsForAccessCode) + val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header)) return runTaskAsyncReturnOnMain( runnable = ScanProductTask(null, userTokensRepository, additionalBlockchainsToDerive), @@ -175,6 +180,16 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return context.getString(stringResId, formatArgs) } + fun setAccessCodeRequestPolicy( + useBiometricsForAccessCode: Boolean, + ) { + tangemSdk.config.userCodeRequestPolicy = if (useBiometricsForAccessCode) { + UserCodeRequestPolicy.AlwaysWithBiometrics(codeType = UserCodeType.AccessCode) + } else { + UserCodeRequestPolicy.Default + } + } + companion object { val config = Config( linkedTerminal = true, diff --git a/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt new file mode 100644 index 0000000000..f649f4919f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt @@ -0,0 +1,34 @@ +package com.tangem.tap.domain.model + +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.util.UserWalletId +import com.tangem.domain.common.util.userWalletId +import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl + +data class UserWallet( + val name: String, + val walletId: UserWalletId, + val artworkUrl: String, + val cardsInWallet: Set, + val scanResponse: ScanResponse, +) { + val cardId: String + get() = scanResponse.card.cardId + + companion object { + suspend operator fun invoke( + scanResponse: ScanResponse, + backupCardsIds: Set? = null, + ): UserWallet { + return with(scanResponse) { + UserWallet( + walletId = card.userWalletId, + name = productType.name, + artworkUrl = card.getOrLoadCardArtworkUrl(), + cardsInWallet = backupCardsIds?.plus(card.cardId) ?: setOf(card.cardId), + scanResponse = this, + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt new file mode 100644 index 0000000000..954888733e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt @@ -0,0 +1,233 @@ +package com.tangem.tap.domain.scanCard + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.common.services.Result +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.extensions.withMainContext +import com.tangem.operations.backup.BackupService +import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE +import com.tangem.tap.backupService +import com.tangem.tap.common.analytics.Analytics +import com.tangem.tap.common.analytics.events.IntroductionProcess +import com.tangem.tap.common.analytics.paramsInterceptor.BatchIdParamsInterceptor +import com.tangem.tap.common.extensions.dispatchDialogShow +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa +import com.tangem.tap.common.redux.AppDialog +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.common.redux.navigation.AppScreen +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.disclaimer.redux.DisclaimerAction +import com.tangem.tap.features.disclaimer.redux.DisclaimerType +import com.tangem.tap.features.disclaimer.redux.isAccepted +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.SaltPayExceptionHandler +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState +import com.tangem.tap.preferencesStorage +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager +import com.tangem.tap.userTokensRepository +import com.tangem.wallet.R +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +// TODO: Create repository for that +object ScanCardProcessor { + suspend fun scan( + useBiometricsForAccessCode: Boolean = false, + additionalBlockchainsToDerive: Collection? = null, + onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {}, + onScanStateChange: suspend (scanInProgress: Boolean) -> Unit = {}, + onWalletNotCreated: suspend (() -> Unit) = {}, + onFailure: suspend (error: TangemError) -> Unit = {}, + onSuccess: suspend (scanResponse: ScanResponse) -> Unit = {}, + ) = withMainContext { + onProgressStateChange(true) + onScanStateChange(true) + tangemSdkManager.scanProduct( + userTokensRepository = userTokensRepository, + additionalBlockchainsToDerive = additionalBlockchainsToDerive, + useBiometricsForAccessCode = useBiometricsForAccessCode, + ) + .doOnFailure { error -> + onProgressStateChange(false) + onScanStateChange(false) + onFailure(error) + } + .doOnSuccess { scanResponse -> + onScanStateChange(false) + checkForUnfinishedBackupForSaltPay( + backupService = backupService, + scanResponse = scanResponse, + onProgressStateChange = { onProgressStateChange(it) }, + nextHandler = { + showDisclaimerIfNeed( + scanResponse = scanResponse, + nextHandler = { + onScanSuccess( + scanResponse = scanResponse, + onProgressStateChange = onProgressStateChange, + onSuccess = onSuccess, + onWalletNotCreated = onWalletNotCreated, + onFailure = onFailure, + ) + }, + ) + }, + ) + + } + } + + /** + * It checks only the SaltPay cards. To check for unfinished backups for the standard Wallet cards + * see BackupAction.CheckForUnfinishedBackup + * If user touches card other than Visa SaltPay - show dialog and block next processing + */ + private inline fun checkForUnfinishedBackupForSaltPay( + backupService: BackupService, + scanResponse: ScanResponse, + onProgressStateChange: (showProgress: Boolean) -> Unit, + nextHandler: (ScanResponse) -> Unit, + ) { + if (!backupService.hasIncompletedBackup || !backupService.primaryCardIsSaltPayVisa()) { + nextHandler(scanResponse) + return + } + + val isTheSamePrimaryCard = backupService.primaryCardId + ?.let { it == scanResponse.card.cardId } + ?: false + + if (scanResponse.isSaltPayWallet() || !isTheSamePrimaryCard) { + onProgressStateChange(false) + showSaltPayTapVisaLogoCardDialog() + } else { + nextHandler(scanResponse) + } + } + + private suspend inline fun showDisclaimerIfNeed( + scanResponse: ScanResponse, + crossinline nextHandler: suspend (ScanResponse) -> Unit, + ) { + val disclaimerType = DisclaimerType.get(scanResponse) + store.dispatch(DisclaimerAction.SetDisclaimerType(disclaimerType)) + + if (disclaimerType.isAccepted()) { + nextHandler((scanResponse)) + } else scope.launch(Dispatchers.Main) { + delay(DELAY_SDK_DIALOG_CLOSE) + store.dispatch( + DisclaimerAction.Show { + scope.launch { + nextHandler(scanResponse) + } + }, + ) + } + } + + private suspend inline fun onScanSuccess( + scanResponse: ScanResponse, + crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit, + crossinline onWalletNotCreated: suspend () -> Unit, + crossinline onSuccess: suspend (ScanResponse) -> Unit, + crossinline onFailure: suspend (error: TangemError) -> Unit, + ) { + Analytics.send(IntroductionProcess.CardWasScanned()) + + val globalState = store.state.globalState + val tapWalletManager = globalState.tapWalletManager + tapWalletManager.updateConfigManager(scanResponse) + + Analytics.addParamsInterceptor(BatchIdParamsInterceptor(scanResponse.card.batchId)) + + store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) + + if (scanResponse.isSaltPay()) { + if (scanResponse.isSaltPayVisa()) { + val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse) + val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse, manager) + delay(500) + withMainContext { + when (result) { + is Result.Success -> { + val isOnboardingCase = result.data + if (isOnboardingCase) { + onWalletNotCreated() + store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false)) + store.dispatch(OnboardingSaltPayAction.SetDependencies(manager, config)) + store.dispatch(OnboardingSaltPayAction.Update) + navigateTo(AppScreen.OnboardingWallet) { onProgressStateChange(it) } + } else { + delay(DELAY_SDK_DIALOG_CLOSE) + onSuccess(scanResponse) + onProgressStateChange(false) + } + } + is Result.Failure -> { + SaltPayExceptionHandler.handle(result.error) + delay(DELAY_SDK_DIALOG_CLOSE) + onFailure(TangemSdkError.ExceptionError(result.error)) + onProgressStateChange(false) + } + } + } + } else { + delay(DELAY_SDK_DIALOG_CLOSE) + if (scanResponse.card.backupStatus?.isActive == false) { + showSaltPayTapVisaLogoCardDialog() + } else { + onSuccess(scanResponse) + } + onProgressStateChange(false) + } + } else { + if (OnboardingHelper.isOnboardingCase(scanResponse)) { + onWalletNotCreated() + store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true)) + val appScreen = OnboardingHelper.whereToNavigate(scanResponse) + navigateTo(appScreen) { onProgressStateChange(it) } + } else { + if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) { + onWalletNotCreated() + store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly)) + navigateTo(AppScreen.OnboardingTwins) { onProgressStateChange(it) } + } else { + delay(DELAY_SDK_DIALOG_CLOSE) + onSuccess(scanResponse) + onProgressStateChange(false) + } + } + } + } + + private fun showSaltPayTapVisaLogoCardDialog() { + store.dispatchDialogShow( + AppDialog.SimpleOkDialogRes( + headerId = R.string.saltpay_error_empty_backup_title, + messageId = R.string.saltpay_error_empty_backup_message, + ), + ) + } + + private suspend inline fun navigateTo( + screen: AppScreen, + onProgressStateChange: (showProgress: Boolean) -> Unit, + ) { + delay(DELAY_SDK_DIALOG_CLOSE) + store.dispatchOnMain(NavigationAction.NavigateTo(screen)) + onProgressStateChange(false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt new file mode 100644 index 0000000000..05efbdf35f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt @@ -0,0 +1,29 @@ +package com.tangem.tap.domain.userWalletList + +import com.tangem.common.CompletionResult +import com.tangem.domain.common.util.UserWalletId +import com.tangem.tap.domain.model.UserWallet +import kotlinx.coroutines.flow.Flow + +interface UserWalletsListManager { + val userWallets: Flow> + val selectedUserWallet: Flow + val selectedUserWalletSync: UserWallet? + val isLocked: Flow + val isLockedSync: Boolean + val hasSavedUserWallets: Boolean + + suspend fun unlockWithBiometry(): CompletionResult + suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult + fun lock() + + suspend fun selectWallet(walletId: UserWalletId): CompletionResult + + suspend fun save(userWallet: UserWallet): CompletionResult + suspend fun update(userWallet: UserWallet): CompletionResult + + suspend fun delete(walletIds: List): CompletionResult + suspend fun clear(): CompletionResult + + companion object +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListProvider.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListProvider.kt new file mode 100644 index 0000000000..f60c7f3de0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListProvider.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.domain.userWalletList.di + +import com.tangem.tap.domain.userWalletList.UserWalletsListManager +import com.tangem.tap.domain.userWalletList.implementation.DummyUserWalletsListManager + +fun UserWalletsListManager.Companion.provideDummyImplementation(): UserWalletsListManager { + return DummyUserWalletsListManager() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt new file mode 100644 index 0000000000..bcc6c05b7f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt @@ -0,0 +1,58 @@ +package com.tangem.tap.domain.userWalletList.implementation + +import com.tangem.common.CompletionResult +import com.tangem.common.catching +import com.tangem.domain.common.util.UserWalletId +import com.tangem.tap.domain.model.UserWallet +import com.tangem.tap.domain.userWalletList.UserWalletsListManager +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +class DummyUserWalletsListManager : UserWalletsListManager { + override val userWallets: Flow> + get() = flowOf(emptyList()) + override val selectedUserWallet: Flow + get() = flowOf() + override val selectedUserWalletSync: UserWallet? + get() = null + override val isLocked: Flow + get() = flowOf(true) + override val isLockedSync: Boolean + get() = true + override val hasSavedUserWallets: Boolean + get() = false + + override suspend fun unlockWithBiometry(): CompletionResult { + return CompletionResult.Success(null) + } + + override suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult { + return CompletionResult.Success(Unit) + } + + override fun lock() { + /* no-op */ + } + + override suspend fun selectWallet(walletId: UserWalletId): CompletionResult { + return catching { + error("Not implemented") + } + } + + override suspend fun save(userWallet: UserWallet): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun update(userWallet: UserWallet): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun delete(walletIds: List): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun clear(): CompletionResult { + return CompletionResult.Success(Unit) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt new file mode 100644 index 0000000000..eb7fca3d46 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -0,0 +1,90 @@ +package com.tangem.tap.features.welcome.redux + +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.domain.common.ScanResponse +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.onUserWalletSelected +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.navigation.AppScreen +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.domain.model.UserWallet +import com.tangem.tap.domain.scanCard.ScanCardProcessor +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.userWalletsListManager +import kotlinx.coroutines.launch +import org.rekotlin.Middleware + +internal class WelcomeMiddleware { + val middleware: Middleware = { _, _ -> + { next -> + { action -> + if (action is WelcomeAction) { + handleAction(action) + } + next(action) + } + } + } + + private fun handleAction(action: WelcomeAction) { + when (action) { + is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometry() + is WelcomeAction.ProceedWithCard -> proceedWithCard() + is WelcomeAction.ProceedWithBiometrics.Error -> Unit + is WelcomeAction.ProceedWithCard.Error -> Unit + is WelcomeAction.ProceedWithBiometrics.Success -> Unit + is WelcomeAction.ProceedWithCard.Success -> Unit + is WelcomeAction.CloseError -> Unit + } + } + + private fun proceedWithBiometry() { + scope.launch { + userWalletsListManager.unlockWithBiometry() + .doOnFailure { error -> + store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Error(error)) + } + .doOnSuccess { selectedUserWallet -> + if (selectedUserWallet != null) { + store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) + store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Success) + store.onUserWalletSelected(selectedUserWallet) + } + } + } + } + + private fun proceedWithCard() = scope.launch { + scanCardInternal { scanResponse -> + val userWallet = UserWallet(scanResponse) + + userWalletsListManager.unlockWithCard(userWallet) + .doOnFailure { error -> + store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(error)) + } + .doOnSuccess { + store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) + store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success) + store.onUserWalletSelected(userWallet) + } + } + } + + private suspend inline fun scanCardInternal( + crossinline onCardScanned: suspend (ScanResponse) -> Unit, + ) { + ScanCardProcessor.scan( + onSuccess = { scanResponse -> + scope.launch { onCardScanned(scanResponse) } + }, + onFailure = { + store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(it)) + }, + onWalletNotCreated = { + store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success) + }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt new file mode 100644 index 0000000000..d5f3e0276b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.features.welcome.redux + +import com.tangem.tap.common.redux.AppState +import org.rekotlin.Action + +internal object WelcomeReducer { + fun reduce(action: Action, state: AppState): WelcomeState { + return if (action is WelcomeAction) { + internalReduce(action, state.welcomeState) + } else state.welcomeState + } + + private fun internalReduce(action: WelcomeAction, state: WelcomeState): WelcomeState { + return when (action) { + is WelcomeAction.ProceedWithBiometrics -> state.copy(isUnlockWithBiometricsInProgress = true) + is WelcomeAction.ProceedWithCard -> state.copy(isUnlockWithCardInProgress = true) + is WelcomeAction.ProceedWithBiometrics.Error -> state.copy( + error = action.error, + isUnlockWithBiometricsInProgress = false, + ) + is WelcomeAction.ProceedWithCard.Error -> state.copy( + error = action.error, + isUnlockWithCardInProgress = false, + ) + is WelcomeAction.ProceedWithBiometrics.Success -> state.copy(isUnlockWithBiometricsInProgress = false) + is WelcomeAction.ProceedWithCard.Success -> state.copy(isUnlockWithCardInProgress = false) + is WelcomeAction.CloseError -> state.copy(error = null) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt index 2d89c4d902..a4fce23f93 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt @@ -1,5 +1,10 @@ package com.tangem.tap.features.welcome.redux +import com.tangem.common.core.TangemError import org.rekotlin.StateType -class WelcomeState : StateType \ No newline at end of file +data class WelcomeState( + val isUnlockWithBiometricsInProgress: Boolean = false, + val isUnlockWithCardInProgress: Boolean = false, + val error: TangemError? = null, +) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt index b80c8ac31b..07c9af833c 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt @@ -20,6 +20,7 @@ import androidx.fragment.app.viewModels import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.fragments.ComposeFragment import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.details.ui.cardsettings.resolveReference import com.tangem.tap.features.welcome.ui.components.WelcomeScreenContent import com.tangem.wallet.R @@ -37,7 +38,7 @@ internal class WelcomeFragment : ComposeFragment() { state: WelcomeScreenState, ) { val snackbarHostState = remember { SnackbarHostState() } - val errorMessage by rememberUpdatedState(newValue = state.error?.customMessage) + val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference()) val backgroundColor = colorResource(id = R.color.background_primary) SystemBarsEffect { diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt index 7a741a391f..d0d883148a 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt @@ -1,11 +1,11 @@ package com.tangem.tap.features.welcome.ui import androidx.compose.runtime.Immutable -import com.tangem.common.core.TangemError +import com.tangem.tap.features.details.ui.cardsettings.TextReference @Immutable internal data class WelcomeScreenState( val showUnlockWithBiometricsProgress: Boolean = false, val showUnlockWithCardProgress: Boolean = false, - val error: TangemError? = null, + val error: TextReference? = null, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt index ab9e8e6673..a047d8518e 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt @@ -1,6 +1,8 @@ package com.tangem.tap.features.welcome.ui import androidx.lifecycle.ViewModel +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.welcome.redux.WelcomeAction import com.tangem.tap.features.welcome.redux.WelcomeState import com.tangem.tap.store @@ -15,6 +17,7 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber { init { subscribeToStoreChanges() + initGlobalState() } fun unlockWallets() { @@ -31,8 +34,16 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber { override fun newState(state: WelcomeState) { stateInternal.update { prevState -> - // TODO: Update screen state - prevState + prevState.copy( + showUnlockWithBiometricsProgress = state.isUnlockWithBiometricsInProgress, + showUnlockWithCardProgress = state.isUnlockWithCardInProgress, + error = state.error + ?.takeUnless { it.silent } + ?.let { error -> + error.messageResId?.let { TextReference.Res(it) } + ?: TextReference.Str(error.customMessage) + }, + ) } } @@ -46,4 +57,10 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber { .select { it.welcomeState } } } + + private fun initGlobalState() { + store.dispatch(GlobalAction.RestoreAppCurrency) + store.dispatch(GlobalAction.ExchangeManager.Init) + store.dispatch(GlobalAction.FetchUserCountry) + } } \ No newline at end of file