Updated on 2026-08-14

This commit is contained in:
Tangem 2022-12-14 19:16:49 +03:00
commit c6c1fd3044
42 changed files with 430 additions and 297 deletions

View file

@ -121,8 +121,12 @@ class DetailsMiddleware {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(selectedUserWallet)
if (userWalletsListManager.isLockedSync) {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
} else {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(selectedUserWallet)
}
} else {
userWalletsListManager.lock()
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
@ -232,6 +236,7 @@ class DetailsMiddleware {
}
private fun enrollBiometrics() {
Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication)
store.dispatchOnMain(NavigationAction.OpenBiometricsSettings)
}
@ -268,14 +273,18 @@ class DetailsMiddleware {
Timber.e(error, "Wallet saving failed")
}
.doOnSuccess {
Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.On))
preferencesStorage.shouldShowSaveUserWalletScreen = false
preferencesStorage.shouldSaveUserWallets = true
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
setting = PrivacySetting.SaveWallets,
enable = true,
),
)
store.onUserWalletSelected(userWallet)
}
}
@ -284,22 +293,29 @@ class DetailsMiddleware {
userWalletsListManager.clear()
.flatMap { walletStoresManager.clear() }
.doOnSuccess {
Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.Off))
preferencesStorage.shouldSaveUserWallets = false
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
setting = PrivacySetting.SaveWallets,
enable = false,
),
)
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
}
private fun saveAccessCodes(state: DetailsState) {
Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.On))
preferencesStorage.shouldSaveAccessCodes = true
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = state.scanResponse?.card?.isAccessCodeSet == true,
)
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
setting = PrivacySetting.SaveAccessCode,
@ -311,10 +327,13 @@ class DetailsMiddleware {
private suspend fun deleteSavedAccessCodes() {
tangemSdkManager.clearSavedUserCodes()
.doOnSuccess {
Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.Off))
preferencesStorage.shouldSaveAccessCodes = false
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = false,
)
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
setting = PrivacySetting.SaveAccessCode,

View file

@ -93,9 +93,14 @@ private fun readCard() = scope.launch {
userWalletsListManager.save(userWallet)
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
tangemSdkManager.setAccessCodeRequestPolicy(useBiometricsForAccessCode = false)
store.onCardScanned(scanResponse)
}
.doOnSuccess {
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
userWallet.hasAccessCode,
)
store.onUserWalletSelected(userWallet)
}
.doOnResult {

View file

@ -1,16 +1,20 @@
package com.tangem.tap.features.onboarding
import com.tangem.common.doOnSuccess
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -55,6 +59,18 @@ class OnboardingHelper {
backupCardsIds: List<String>? = null,
) {
when {
// When should save user wallets but manager is locked, then unlock manager with card
preferencesStorage.shouldSaveUserWallets &&
userWalletsListManager.isLockedSync -> scope.launch {
val userWallet = UserWalletBuilder(scanResponse).build()
tangemSdkManager.setAccessCodeRequestPolicy(useBiometricsForAccessCode = false)
userWalletsListManager.unlockWithCard(userWallet)
.doOnSuccess {
store.onUserWalletSelected(userWallet)
}
}
// When should save user wallets, then save card without navigate to save wallet screen
preferencesStorage.shouldSaveUserWallets -> scope.launch {
store.dispatchOnMain(
SaveWalletAction.ProvideBackupInfo(
@ -65,6 +81,8 @@ class OnboardingHelper {
)
store.dispatchOnMain(SaveWalletAction.Save)
}
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
// then open save wallet screen
tangemSdkManager.canUseBiometry &&
preferencesStorage.shouldShowSaveUserWalletScreen -> scope.launch {
delay(timeMillis = 1_200)
@ -77,6 +95,7 @@ class OnboardingHelper {
)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.SaveWallet))
}
// If device has no biometry and save wallet screen has been shown, then go through old scenario
else -> scope.launch {
store.onCardScanned(scanResponse)
}

View file

@ -234,7 +234,6 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
when (action) {
is BackupAction.StartBackup -> {
tangemSdkManager.setAccessCodeRequestPolicy(useBiometricsForAccessCode = false)
Analytics.send(Onboarding.Backup.Started())
backupService.discardSavedBackup()
val primaryCard = scanResponse?.primaryCard

View file

@ -4,6 +4,10 @@ import com.tangem.common.CompletionResult
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.flatMap
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
@ -36,9 +40,9 @@ internal class SaveWalletMiddleware {
is SaveWalletAction.Save -> saveWalletIfBiometricsEnrolled(state)
is SaveWalletAction.EnrollBiometrics.Enroll -> enrollBiometrics()
is SaveWalletAction.SaveWalletWasShown -> saveWalletWasShown()
is SaveWalletAction.Dismiss -> dismiss(state)
is SaveWalletAction.Save.Success,
is SaveWalletAction.ProvideBackupInfo,
is SaveWalletAction.Dismiss,
is SaveWalletAction.CloseError,
is SaveWalletAction.Save.Error,
is SaveWalletAction.EnrollBiometrics,
@ -74,6 +78,13 @@ internal class SaveWalletMiddleware {
?: store.state.globalState.scanResponse
?: return
if (state.backupInfo != null) {
// TODO: Remove after onboarding refactoring
Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.On))
} else {
Analytics.send(MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.On))
}
scope.launch {
val userWallet = UserWalletBuilder(scanResponse)
.backupCardsIds(state.backupInfo?.backupCardsIds)
@ -83,6 +94,7 @@ internal class SaveWalletMiddleware {
saveAccessCodeIfNeeded(state.backupInfo?.accessCode, userWallet.cardsInWallet)
.flatMap { userWalletsListManager.save(userWallet, canOverride = true) }
.flatMap { userWalletsListManager.selectWallet(userWallet.walletId) }
.doOnFailure { error ->
store.dispatchOnMain(SaveWalletAction.Save.Error(error))
}
@ -93,21 +105,29 @@ internal class SaveWalletMiddleware {
preferencesStorage.shouldSaveAccessCodes = isFirstSavedWallet ||
preferencesStorage.shouldSaveAccessCodes
val isSavedWalletSelected =
userWalletsListManager.selectedUserWalletSync?.walletId == userWallet.walletId
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
userWallet.hasAccessCode,
)
store.dispatchOnMain(SaveWalletAction.Save.Success)
if (isSavedWalletSelected) {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(userWallet)
} else {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.WalletSelector))
}
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(userWallet)
}
}
}
private fun dismiss(state: SaveWalletState) {
if (state.backupInfo != null) {
// TODO: Remove after onboarding refactoring
Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.Off))
} else {
Analytics.send(MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.Off))
}
}
private fun saveWalletWasShown() {
preferencesStorage.shouldShowSaveUserWalletScreen = false
}
@ -118,13 +138,10 @@ internal class SaveWalletMiddleware {
): CompletionResult<Unit> {
return when {
accessCode != null -> {
tangemSdkManager.unlockBiometricKeys()
.flatMap {
tangemSdkManager.saveAccessCode(
accessCode = accessCode,
cardsIds = cardsInWallet,
)
}
tangemSdkManager.saveAccessCode(
accessCode = accessCode,
cardsIds = cardsInWallet,
)
}
else -> {
CompletionResult.Success(Unit)

View file

@ -5,12 +5,10 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.accompanist.appcompattheme.AppCompatTheme
@ -22,6 +20,8 @@ import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.BaseFragment
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.tokens.redux.ContractAddress
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
import com.tangem.tap.features.tokens.redux.TokensAction
@ -40,8 +40,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.rekotlin.StoreSubscriber
class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
StoreSubscriber<TokensState> {
class AddTokensFragment : BaseFragment(R.layout.fragment_add_tokens), StoreSubscriber<TokensState> {
private val binding: FragmentAddTokensBinding by viewBinding(FragmentAddTokensBinding::bind)
private var tokensState: MutableState<TokensState> = mutableStateOf(store.state.tokensState)
@ -51,33 +50,7 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
Analytics.send(ManageTokens.ScreenOpened())
activity?.onBackPressedDispatcher?.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(TokensAction.ResetState)
}
},
)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.tokensState == newState.tokensState
}.select { it.tokensState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
addBackPressHandler(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(binding) {
@ -96,11 +69,7 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
}
val onLoadMore = {
store.dispatch(
TokensAction.LoadMore(
scanResponse = store.state.globalState.scanResponse,
),
)
store.dispatch(TokensAction.LoadMore(scanResponse = store.state.globalState.scanResponse))
}
cvCurrencies.setContent {
@ -115,24 +84,23 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
}
}
override fun newState(state: TokensState) {
if (activity == null || view == null) return
val toolbarTitle =
if (state.allowToAdd) R.string.main_manage_tokens else R.string.search_tokens_title
tokensState.value = state
binding.toolbar.title = getString(toolbarTitle)
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state
.skipRepeats { oldState, newState -> oldState.tokensState == newState.tokensState }
.select { it.tokensState }
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_search -> true
R.id.menu_navigate_add_custom_token -> {
Analytics.send(ManageTokens.ButtonCustomToken())
store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken)
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onDestroy() {
store.dispatch(TokensAction.ResetState)
super.onDestroy()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -156,25 +124,50 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
return super.onCreateOptionsMenu(menu, inflater)
}
override fun onDestroy() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_search -> true
R.id.menu_navigate_add_custom_token -> {
Analytics.send(ManageTokens.ButtonCustomToken())
store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken)
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun configureTransitions() {
super.configureTransitions()
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun handleOnBackPressed() {
super.handleOnBackPressed()
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(TokensAction.ResetState)
super.onDestroy()
}
override fun newState(state: TokensState) {
if (activity == null || view == null) return
val toolbarTitle =
if (state.allowToAdd) R.string.main_manage_tokens else R.string.search_tokens_title
tokensState.value = state
binding.toolbar.title = getString(toolbarTitle)
}
}
fun SearchView.inputtedTextAsFlow(): Flow<String> = callbackFlow {
val watcher =
setOnQueryTextListener(
object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
val watcher = setOnQueryTextListener(
object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean = false
override fun onQueryTextChange(newText: String?): Boolean {
trySend(newText ?: "")
return false
}
},
)
override fun onQueryTextChange(newText: String?): Boolean {
trySend(newText ?: "")
return false
}
},
)
awaitClose { (watcher) }
}

View file

@ -83,25 +83,21 @@ fun CurrenciesScreen(
Scaffold(
floatingActionButton = {
if (tokensState.value.allowToAdd) SaveChangesButton(isKeyboardOpen) {
onSaveChanges(addedTokensState.value, addedBlockchainsState.value)
if (tokensState.value.allowToAdd) {
SaveChangesButton(isKeyboardOpen) {
onSaveChanges(addedTokensState.value, addedBlockchainsState.value)
}
}
},
floatingActionButtonPosition = FabPosition.Center,
) {
AnimatedVisibility(
visible = tokensState.value.loadCoinsState == LoadCoinsState.LOADING,
enter = fadeIn(),
exit = fadeOut(),
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize(),
) {
CircularProgressIndicator(
color = Color(0xFF1ACE80),
)
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(color = Color(0xFF1ACE80))
}
}
AnimatedVisibility(
@ -148,21 +144,19 @@ private fun toggleBlockchain(
currencySymbol = blockchain.currency,
),
)
} else if (isAddedOnMainScreen) {
store.dispatchDialogShow(
WalletDialog.RemoveWalletDialog(
currencyTitle = blockchain.name,
onOk = {
analyticsCurrencyTypeParam.sendOn()
addedBlockchainsState.removeAndNotify(blockchain)
},
),
)
} else {
if (isAddedOnMainScreen) {
store.dispatchDialogShow(
WalletDialog.RemoveWalletDialog(
currencyTitle = blockchain.name,
onOk = {
analyticsCurrencyTypeParam.sendOn()
addedBlockchainsState.removeAndNotify(blockchain)
},
),
)
} else {
analyticsCurrencyTypeParam.sendOff()
addedBlockchainsState.removeAndNotify(blockchain)
}
analyticsCurrencyTypeParam.sendOff()
addedBlockchainsState.removeAndNotify(blockchain)
}
} else {
analyticsCurrencyTypeParam.sendOn()

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.domain.tokens.Contract
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.tokens.redux.ContractAddress
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
@ -30,22 +31,20 @@ fun CurrencyExpandedContent(
exit = fadeOut() + shrinkVertically(),
) {
val blockchains = currency.contracts.map { it.blockchain }
Column(
modifier = Modifier
.fillMaxWidth(),
) {
Column(modifier = Modifier.fillMaxWidth()) {
blockchains.mapIndexed { index, blockchain ->
val contract = currency.contracts.firstOrNull { it.blockchain == blockchain }
val currencyContract = currency.contracts
.firstOrNull { it.blockchain == blockchain }
?: return@mapIndexed
val added = if (contract.address != null) {
addedTokens.map { it.token.contractAddress }.contains(contract.address)
val added = if (currencyContract.address != null) {
addedTokens.any(currencyContract::isInclude)
} else {
addedBlockchains.contains(blockchain)
}
NetworkItem(
currency = currency,
contract = contract,
contract = currencyContract,
blockchain = blockchain,
allowToAdd = allowToAdd,
added = added,
@ -57,4 +56,7 @@ fun CurrencyExpandedContent(
}
}
}
}
}
private fun Contract.isInclude(addedToken: TokenWithBlockchain): Boolean =
address == addedToken.token.contractAddress && blockchain == addedToken.blockchain

View file

@ -8,6 +8,7 @@ import com.tangem.common.extensions.guard
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
@ -181,6 +182,7 @@ class MultiWalletMiddleware {
selectedWallet: UserWallet,
state: WalletState?,
) = scope.launch {
Analytics.send(MainScreen.CardWasScanned())
ScanCardProcessor.scan(
cardId = selectedWallet.cardId,
additionalBlockchainsToDerive = state?.missingDerivations?.map { it.blockchain },

View file

@ -12,6 +12,7 @@ import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
@ -349,9 +350,11 @@ class WalletMiddleware {
private fun changeWallet() {
when {
userWalletsListManager.hasSavedUserWallets -> {
Analytics.send(MainScreen.ButtonMyWallets())
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.WalletSelector))
}
else -> {
Analytics.send(MainScreen.ButtonScanCard())
store.dispatch(WalletAction.Scan)
}
}

View file

@ -51,7 +51,6 @@ class MultiWalletView : WalletView() {
btnAddToken.show()
}
override fun onViewCreated() {
setupWalletsRecyclerView()
}
@ -69,7 +68,7 @@ class MultiWalletView : WalletView() {
val fragment = fragment ?: return
val binding = binding ?: return
handleTotalBalance(binding, state.totalBalance, state.state)
handleTotalBalance(binding, state.totalBalance, state.state, state.walletsData.size)
handleBackupWarning(binding, state.showBackupWarning)
handleRescanWarning(binding, state.missingDerivations.isNotEmpty())
setupWalletCardNumber(binding, state.walletCardsCount)
@ -137,29 +136,34 @@ class MultiWalletView : WalletView() {
binding: FragmentWalletBinding,
totalBalance: TotalBalance?,
progressState: ProgressState,
walletsCount: Int,
) = with(binding.lCardTotalBalance) {
if (totalBalance == null) {
veilBalance.animateVisibility(show = true)
root.isVisible = progressState == ProgressState.Loading
if (walletsCount == 0) {
root.isVisible = false
} else {
root.isVisible = true
if (totalBalance == null) {
veilBalance.animateVisibility(show = true)
root.isVisible = progressState == ProgressState.Loading
} else {
root.isVisible = true
// Skip changes when on refreshing state
if (totalBalance.state == ProgressState.Refreshing || progressState == ProgressState.Refreshing) {
return@with
}
// Skip changes when on refreshing state
if (totalBalance.state == ProgressState.Refreshing || progressState == ProgressState.Refreshing) {
return@with
}
veilBalance.animateVisibility(show = totalBalance.state == ProgressState.Loading)
tvBalance.animateVisibility(show = totalBalance.state != ProgressState.Loading)
tvProcessing.animateVisibility(show = totalBalance.state == ProgressState.Error)
veilBalance.animateVisibility(show = totalBalance.state == ProgressState.Loading)
tvBalance.animateVisibility(show = totalBalance.state != ProgressState.Loading)
tvProcessing.animateVisibility(show = totalBalance.state == ProgressState.Error)
tvBalance.text = totalBalance.fiatAmount.formatAmountAsSpannedString(
currencySymbol = totalBalance.fiatCurrency.symbol,
)
tvCurrencyName.text = totalBalance.fiatCurrency.code
tvBalance.text = totalBalance.fiatAmount.formatAmountAsSpannedString(
currencySymbol = totalBalance.fiatCurrency.symbol,
)
tvCurrencyName.text = totalBalance.fiatCurrency.code
tvCurrencyName.setOnClickListener {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
tvCurrencyName.setOnClickListener {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
}
}
}

View file

@ -8,15 +8,16 @@ data class UserWalletModel(
val artworkUrl: String,
val type: Type,
val fiatBalance: TotalFiatBalance,
val isLocked: Boolean,
) {
sealed interface Type {
data class SingleCurrency(
val blockchainName: String? = null,
val blockchainName: String,
) : Type
data class MultiCurrency(
val cardsInWallet: Int,
val tokensCount: Int = 0,
val tokensCount: Int,
) : Type
}
}

View file

@ -37,6 +37,10 @@ internal sealed interface WalletSelectorAction : Action {
val walletId: String,
) : WalletSelectorAction
data class UnlockWalletWithCard(
val walletId: String,
) : WalletSelectorAction
data class RenameWallet(
val walletId: String,
val newName: String,
@ -56,5 +60,6 @@ internal sealed interface WalletSelectorAction : Action {
) : WalletSelectorAction
data class HandleError(val error: TangemError) : WalletSelectorAction
object CloseError : WalletSelectorAction
}

View file

@ -7,6 +7,8 @@ import com.tangem.common.flatMap
import com.tangem.common.map
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.MyWallets
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
@ -24,7 +26,6 @@ import com.tangem.tap.tangemSdkManager
import com.tangem.tap.totalFiatBalanceCalculator
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletStoresManager
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
@ -60,6 +61,9 @@ internal class WalletSelectorMiddleware {
is WalletSelectorAction.SelectWallet -> {
selectWallet(action.walletId)
}
is WalletSelectorAction.UnlockWalletWithCard -> {
unlockWalletWithCard(action.walletId)
}
is WalletSelectorAction.RemoveWallets -> {
removeWallets(action.walletIdsToRemove, state)
}
@ -105,6 +109,8 @@ internal class WalletSelectorMiddleware {
}
private fun unlockWalletsWithBiometry() {
Analytics.send(MyWallets.Button.UnlockWithBiometrics)
scope.launch {
userWalletsListManager.unlockWithBiometry()
.doOnFailure { error ->
@ -117,6 +123,8 @@ internal class WalletSelectorMiddleware {
}
private fun addWallet() = scope.launch {
Analytics.send(MyWallets.Button.ScanNewCard)
scanCardInternal { scanResponse ->
val userWallet = UserWalletBuilder(scanResponse).build()
@ -125,15 +133,13 @@ internal class WalletSelectorMiddleware {
store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error))
}
.doOnSuccess {
val selectedWallet = userWalletsListManager.selectedUserWallet.first()
val isSavedWalletSelected = userWallet == selectedWallet
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
Analytics.send(MyWallets.CardWasScanned)
if (isSavedWalletSelected) {
updateAccessCodeRequestPolicy(selectedWallet)
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(selectedWallet)
}
userWalletsListManager.selectWallet(userWallet.walletId)
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
updateAccessCodeRequestPolicy(userWallet)
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(userWallet)
}
}
}
@ -152,7 +158,43 @@ internal class WalletSelectorMiddleware {
}
}
private fun unlockWalletWithCard(id: String) {
scope.launch {
userWalletsListManager.get(UserWalletId(id))
.flatMap { userWallet ->
updateUserWalletWithScannedCard(userWallet)
}
.flatMap { updatedUserWallet ->
unlockUserWallet(updatedUserWallet)
}
.doOnFailure { error ->
store.dispatchOnMain(WalletSelectorAction.HandleError(error))
}
}
}
private suspend fun updateUserWalletWithScannedCard(userWallet: UserWallet): CompletionResult<UserWallet> {
return tangemSdkManager.scanCard(userWallet.cardId)
.map { scannedCard ->
userWallet.copy(
scanResponse = userWallet.scanResponse.copy(
card = scannedCard,
),
)
}
}
private suspend fun unlockUserWallet(userWallet: UserWallet): CompletionResult<Unit> {
return userWalletsListManager.unlockWithCard(userWallet)
.doOnSuccess {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(userWallet)
}
}
private fun removeWallets(walletIdsToRemove: List<String>, state: WalletSelectorState) {
Analytics.send(MyWallets.Button.DeleteWalletTapped)
scope.launch {
when (walletIdsToRemove.size) {
state.wallets.size -> clearUserWallets()
@ -165,6 +207,8 @@ internal class WalletSelectorMiddleware {
}
private fun renameWallet(walletId: String, newName: String) {
Analytics.send(MyWallets.Button.EditWalletTapped)
scope.launch {
userWalletsListManager.get(walletId = UserWalletId(walletId))
.map { it.copy(name = newName) }
@ -221,7 +265,7 @@ internal class WalletSelectorMiddleware {
private fun updateAccessCodeRequestPolicy(userWallet: UserWallet) {
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
userWallet.scanResponse.card.isAccessCodeSet,
userWallet.hasAccessCode,
)
}
@ -233,13 +277,7 @@ internal class WalletSelectorMiddleware {
is UserWalletModel.Type.MultiCurrency -> type.copy(
tokensCount = walletStores.flatMap { it.walletsData }.size,
)
is UserWalletModel.Type.SingleCurrency -> type.copy(
blockchainName = walletStores
.firstOrNull()
?.blockchainNetwork
?.blockchain
?.fullName,
)
is UserWalletModel.Type.SingleCurrency -> type
},
fiatBalance = totalFiatBalanceCalculator.calculate(
prevAmount = fiatBalance.amount,

View file

@ -56,6 +56,7 @@ internal object WalletSelectorReducer {
)
is WalletSelectorAction.WalletStoresChanged,
is WalletSelectorAction.SelectWallet,
is WalletSelectorAction.UnlockWalletWithCard,
is WalletSelectorAction.RemoveWallets,
is WalletSelectorAction.RenameWallet,
-> state
@ -66,10 +67,14 @@ internal object WalletSelectorReducer {
return this.map { userWallet ->
prevWallets
.find { it.id == userWallet.walletId.stringValue }
?.copy(
name = userWallet.name,
artworkUrl = userWallet.artworkUrl,
)
?.let {
it.copy(
name = userWallet.name,
artworkUrl = userWallet.artworkUrl,
isLocked = userWallet.isLocked,
type = userWallet.getType(prevType = it.type),
)
}
?: with(userWallet) {
UserWalletModel(
id = walletId.stringValue,
@ -77,6 +82,7 @@ internal object WalletSelectorReducer {
artworkUrl = artworkUrl,
type = getType(),
fiatBalance = TotalFiatBalance.Loading,
isLocked = isLocked,
)
}
}
@ -90,15 +96,18 @@ internal object WalletSelectorReducer {
}
}
private fun UserWallet.getType(): UserWalletModel.Type {
private fun UserWallet.getType(prevType: UserWalletModel.Type? = null): UserWalletModel.Type {
return if (scanResponse.card.isMultiwalletAllowed) {
UserWalletModel.Type.MultiCurrency(
cardsInWallet = (scanResponse.card.backupStatus as? CardDTO.BackupStatus.Active)
?.cardCount?.inc()
?: 1,
tokensCount = (prevType as? UserWalletModel.Type.MultiCurrency)?.tokensCount ?: 0,
)
} else {
UserWalletModel.Type.SingleCurrency()
UserWalletModel.Type.SingleCurrency(
blockchainName = scanResponse.getBlockchain().fullName,
)
}
}
}

View file

@ -60,6 +60,7 @@ private fun List<UserWalletModel>.toUiModels(
name = name,
imageUrl = artworkUrl,
balance = balance,
isLocked = isLocked,
cardsInWallet = type.cardsInWallet,
tokensCount = type.tokensCount,
)
@ -68,6 +69,7 @@ private fun List<UserWalletModel>.toUiModels(
name = name,
imageUrl = artworkUrl,
balance = balance,
isLocked = isLocked,
tokenName = type.blockchainName ?: "",
)
}

View file

@ -1,5 +1,7 @@
package com.tangem.tap.features.walletSelector.ui
import android.app.Dialog
import android.os.Bundle
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -14,6 +16,8 @@ import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.fragment.app.viewModels
import com.tangem.core.ui.fragments.ComposeBottomSheetFragment
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.MyWallets
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.tap.features.walletSelector.ui.components.RenameWalletDialogContent
import com.tangem.tap.features.walletSelector.ui.components.WalletSelectorScreenContent
@ -22,6 +26,12 @@ import com.tangem.tap.features.walletSelector.ui.model.RenameWalletDialog
internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<WalletSelectorScreenState>() {
private val viewModel by viewModels<WalletSelectorViewModel>()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
Analytics.send(MyWallets.MyWalletsScreenOpened)
return super.onCreateDialog(savedInstanceState)
}
@Composable
override fun provideState(): State<WalletSelectorScreenState> {
return viewModel.state.collectAsState()

View file

@ -37,7 +37,9 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
fun walletClicked(walletId: String) = with(state.value) {
when {
isLocked -> Unit
isLocked -> {
store.dispatch(WalletSelectorAction.UnlockWalletWithCard(walletId))
}
editingWalletsIds.isNotEmpty() && !editingWalletsIds.contains(walletId) -> {
editWallet(walletId)
}

View file

@ -14,6 +14,7 @@ internal object MockData {
),
name = "Wallet",
imageUrl = "https://app.tangem.com/cards/card_default.png",
isLocked = false,
tokensCount = 12,
cardsInWallet = 3,
)
@ -26,6 +27,7 @@ internal object MockData {
),
name = "Wallet",
imageUrl = "https://app.tangem.com/cards/card_default.png",
isLocked = false,
tokenName = "Ethereum",
)

View file

@ -72,7 +72,6 @@ internal fun WalletSelectorScreenContent(
singleCurrencyWallets = state.singleCurrencyWallets,
selectedWalletId = state.selectedWalletId,
checkedWalletIds = state.editingWalletsIds,
isLocked = state.isLocked,
onWalletClick = onWalletClick,
onWalletLongClick = onWalletLongClick,
)
@ -141,7 +140,6 @@ private fun WalletsList(
singleCurrencyWallets: List<UserWalletItem>,
selectedWalletId: String?,
checkedWalletIds: List<String>,
isLocked: Boolean,
onWalletClick: (walletId: String) -> Unit,
onWalletLongClick: (walletId: String) -> Unit,
) {
@ -166,7 +164,6 @@ private fun WalletsList(
wallet = wallet,
isSelected = wallet.id == selectedWalletId,
isChecked = wallet.id in checkedWalletIds,
isLocked = isLocked && wallet.id != selectedWalletId,
)
}
}

View file

@ -39,7 +39,6 @@ internal fun WalletItem(
wallet: UserWalletItem,
isSelected: Boolean,
isChecked: Boolean,
isLocked: Boolean,
) {
Row(
modifier = modifier,
@ -59,7 +58,7 @@ internal fun WalletItem(
SpacerW6()
TokensInfo(
modifier = Modifier.weight(weight = .4f),
isLocked = isLocked,
isLocked = wallet.isLocked,
balance = wallet.balance,
tokensCount = (wallet as? MultiCurrencyUserWalletItem)?.tokensCount,
)

View file

@ -1,15 +1,14 @@
package com.tangem.tap.features.walletSelector.ui.model
import androidx.compose.runtime.Immutable
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
@Immutable
internal sealed interface UserWalletItem {
val id: String
val name: String
val imageUrl: String
val balance: Balance
val isLocked: Boolean
val headerText: TextReference
get() = when (this) {
@ -17,7 +16,6 @@ internal sealed interface UserWalletItem {
is SingleCurrencyUserWalletItem -> TextReference.Res(R.string.user_wallet_list_single_header)
}
@Immutable
data class Balance(
val amount: String,
val isLoading: Boolean,
@ -29,6 +27,7 @@ internal data class MultiCurrencyUserWalletItem(
override val name: String,
override val imageUrl: String,
override val balance: UserWalletItem.Balance,
override val isLocked: Boolean,
val tokensCount: Int,
val cardsInWallet: Int,
) : UserWalletItem
@ -38,5 +37,6 @@ internal data class SingleCurrencyUserWalletItem(
override val name: String,
override val imageUrl: String,
override val balance: UserWalletItem.Balance,
override val isLocked: Boolean,
val tokenName: String,
) : UserWalletItem

View file

@ -61,7 +61,7 @@ internal class WelcomeMiddleware {
if (selectedUserWallet != null) {
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
selectedUserWallet.scanResponse.card.isAccessCodeSet,
selectedUserWallet.hasAccessCode,
)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
@ -78,14 +78,12 @@ internal class WelcomeMiddleware {
scanCardInternal { scanResponse ->
val userWallet = UserWalletBuilder(scanResponse).build()
tangemSdkManager.setAccessCodeRequestPolicy(useBiometricsForAccessCode = false)
userWalletsListManager.unlockWithCard(userWallet)
.doOnFailure { error ->
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(error))
}
.doOnSuccess {
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = false,
)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success)
store.onUserWalletSelected(userWallet)