Updated on 2026-08-14

This commit is contained in:
Tangem 2022-11-28 18:08:46 +03:00
parent 0d993b5141
commit e72bf276da
18 changed files with 568 additions and 21 deletions

View file

@ -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)
}

View file

@ -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))
}

View file

@ -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),
)
}

View file

@ -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,
)
}
}

View file

@ -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<AppState> = { dispatch, state ->
val navigationMiddleware: Middleware<AppState> = { _, state ->
{ next ->
{ action ->
if (action is NavigationAction) {
@ -16,16 +18,24 @@ val navigationMiddleware: Middleware<AppState> = { 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<AppState> = { 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)

View file

@ -5,7 +5,7 @@ import org.rekotlin.StateType
import java.lang.ref.WeakReference
data class NavigationState(
val backStack: List<AppScreen> = listOf(AppScreen.Home),
val backStack: List<AppScreen> = emptyList(),
val activity: WeakReference<AppCompatActivity>? = null,
) : StateType

View file

@ -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<Blockchain>? = null,
messageRes: Int? = null,
useBiometricsForAccessCode: Boolean = false,
): CompletionResult<ScanResponse> {
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,

View file

@ -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<String>,
val scanResponse: ScanResponse,
) {
val cardId: String
get() = scanResponse.card.cardId
companion object {
suspend operator fun invoke(
scanResponse: ScanResponse,
backupCardsIds: Set<String>? = 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,
)
}
}
}
}

View file

@ -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<Blockchain>? = 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)
}
}

View file

@ -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<List<UserWallet>>
val selectedUserWallet: Flow<UserWallet>
val selectedUserWalletSync: UserWallet?
val isLocked: Flow<Boolean>
val isLockedSync: Boolean
val hasSavedUserWallets: Boolean
suspend fun unlockWithBiometry(): CompletionResult<UserWallet?>
suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult<Unit>
fun lock()
suspend fun selectWallet(walletId: UserWalletId): CompletionResult<UserWallet>
suspend fun save(userWallet: UserWallet): CompletionResult<Unit>
suspend fun update(userWallet: UserWallet): CompletionResult<Unit>
suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit>
suspend fun clear(): CompletionResult<Unit>
companion object
}

View file

@ -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()
}

View file

@ -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<List<UserWallet>>
get() = flowOf(emptyList())
override val selectedUserWallet: Flow<UserWallet>
get() = flowOf()
override val selectedUserWalletSync: UserWallet?
get() = null
override val isLocked: Flow<Boolean>
get() = flowOf(true)
override val isLockedSync: Boolean
get() = true
override val hasSavedUserWallets: Boolean
get() = false
override suspend fun unlockWithBiometry(): CompletionResult<UserWallet?> {
return CompletionResult.Success(null)
}
override suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
override fun lock() {
/* no-op */
}
override suspend fun selectWallet(walletId: UserWalletId): CompletionResult<UserWallet> {
return catching {
error("Not implemented")
}
}
override suspend fun save(userWallet: UserWallet): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
override suspend fun update(userWallet: UserWallet): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
override suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
override suspend fun clear(): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
}

View file

@ -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<AppState> = { _, _ ->
{ 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)
},
)
}
}

View file

@ -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)
}
}
}

View file

@ -1,5 +1,10 @@
package com.tangem.tap.features.welcome.redux
import com.tangem.common.core.TangemError
import org.rekotlin.StateType
class WelcomeState : StateType
data class WelcomeState(
val isUnlockWithBiometricsInProgress: Boolean = false,
val isUnlockWithCardInProgress: Boolean = false,
val error: TangemError? = null,
) : StateType

View file

@ -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<WelcomeScreenState>() {
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 {

View file

@ -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,
)

View file

@ -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<WelcomeState> {
init {
subscribeToStoreChanges()
initGlobalState()
}
fun unlockWallets() {
@ -31,8 +34,16 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber<WelcomeState> {
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<WelcomeState> {
.select { it.welcomeState }
}
}
private fun initGlobalState() {
store.dispatch(GlobalAction.RestoreAppCurrency)
store.dispatch(GlobalAction.ExchangeManager.Init)
store.dispatch(GlobalAction.FetchUserCountry)
}
}