Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-21 11:10:39 +04:00
parent 688b5da7bd
commit ddf5f8af9f
23 changed files with 0 additions and 2188 deletions

View file

@ -27,7 +27,6 @@ import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.shop.ui.ShopFragment
import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment
import com.tangem.tap.features.walletSelector.ui.WalletSelectorBottomSheetFragment
import com.tangem.tap.features.welcome.ui.WelcomeFragment
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
@ -177,7 +176,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.Swap -> SwapFragment()
AppScreen.Welcome -> WelcomeFragment()
AppScreen.SaveWallet -> SaveWalletBottomSheetFragment()
AppScreen.WalletSelector -> WalletSelectorBottomSheetFragment()
AppScreen.AppCurrencySelector -> AppCurrencySelectorFragment()
AppScreen.ModalNotification -> ModalNotificationBottomSheetFragment()
}

View file

@ -15,7 +15,6 @@ import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.shop.redux.ShopReducer
import com.tangem.tap.features.signin.redux.SignInReducer
import com.tangem.tap.features.tokens.legacy.redux.TokensReducer
import com.tangem.tap.features.walletSelector.redux.WalletSelectorReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphReducer
@ -41,7 +40,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
shopState = ShopReducer.reduce(action, state.shopState),
welcomeState = WelcomeReducer.reduce(action, state),
saveWalletState = SaveWalletReducer.reduce(action, state),
walletSelectorState = WalletSelectorReducer.reduce(action, state),
signInState = SignInReducer.reduce(action, state),
daggerGraphState = DaggerGraphReducer.reduce(action, state),
)

View file

@ -35,8 +35,6 @@ import com.tangem.tap.features.signin.redux.SignInMiddleware
import com.tangem.tap.features.signin.redux.SignInState
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
import com.tangem.tap.features.tokens.legacy.redux.TokensState
import com.tangem.tap.features.walletSelector.redux.WalletSelectorMiddleware
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 com.tangem.tap.proxy.redux.DaggerGraphMiddleware
@ -61,7 +59,6 @@ data class AppState(
val shopState: ShopState = ShopState(),
val welcomeState: WelcomeState = WelcomeState(),
val saveWalletState: SaveWalletState = SaveWalletState(),
val walletSelectorState: WalletSelectorState = WalletSelectorState(),
val signInState: SignInState = SignInState(),
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
) : StateType {
@ -99,7 +96,6 @@ data class AppState(
ShopMiddleware().shopMiddleware,
WelcomeMiddleware().middleware,
SaveWalletMiddleware().middleware,
WalletSelectorMiddleware().middleware,
LockUserWalletsTimerMiddleware().middleware,
AccessCodeRequestPolicyMiddleware().middleware,
SignInMiddleware.middleware,

View file

@ -36,7 +36,6 @@ import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
@ -234,7 +233,6 @@ class DetailsMiddleware {
is DetailsAction.AppSettings.ChangeAppCurrency -> {
store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrency))
store.dispatch(DetailsAction.ChangeAppCurrency(action.fiatCurrency))
store.dispatch(WalletSelectorAction.ChangeAppCurrency(action.fiatCurrency))
}
is DetailsAction.AppSettings.SwitchPrivacySetting.Success,
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure,

View file

@ -1,24 +0,0 @@
package com.tangem.tap.features.walletSelector.redux
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.domain.model.TotalFiatBalance
data class UserWalletModel(
val id: UserWalletId,
val name: String,
val artworkUrl: String,
val type: Type,
val fiatBalance: TotalFiatBalance,
val isLocked: Boolean,
) {
sealed interface Type {
data class SingleCurrency(
val blockchainName: String,
) : Type
data class MultiCurrency(
val cardsInWallet: Int,
val tokensCount: Int,
) : Type
}
}

View file

@ -1,62 +0,0 @@
package com.tangem.tap.features.walletSelector.redux
import com.tangem.common.core.TangemError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.domain.model.WalletStoreModel
import org.rekotlin.Action
internal sealed interface WalletSelectorAction : Action {
data class UserWalletsLoaded(
val userWallets: List<UserWallet>,
) : WalletSelectorAction
data class SelectedWalletChanged(
val selectedWallet: UserWallet,
) : WalletSelectorAction
data class IsLockedChanged(
val isLocked: Boolean,
) : WalletSelectorAction
data class WalletStoresChanged(
val walletsStores: Map<UserWalletId, List<WalletStoreModel>>,
) : WalletSelectorAction
data class BalancesLoaded(
val userWalletModels: List<UserWalletModel>,
) : WalletSelectorAction
object UnlockWithBiometry : WalletSelectorAction {
object Success : WalletSelectorAction
data class Error(val error: TangemError) : WalletSelectorAction
}
data class SelectWallet(
val userWalletId: UserWalletId,
val sendAnalyticsEvent: Boolean = false,
) : WalletSelectorAction
data class RenameWallet(
val userWalletId: UserWalletId,
val newName: String,
) : WalletSelectorAction
data class RemoveWallets(
val userWalletsIds: List<UserWalletId>,
) : WalletSelectorAction
object AddWallet : WalletSelectorAction {
object Success : WalletSelectorAction
data class Error(val error: TangemError) : WalletSelectorAction
}
data class ChangeAppCurrency(
val fiatCurrency: FiatCurrency,
) : WalletSelectorAction
object CloseError : WalletSelectorAction
object ClearUserWallets : WalletSelectorAction
}

View file

@ -1,397 +0,0 @@
package com.tangem.tap.features.walletSelector.redux
import com.tangem.common.*
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.unlockIfLockable
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.events.MyWallets
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.proxy.redux.DaggerGraphState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
// Refactoring is coming
@Suppress("LargeClass")
internal class WalletSelectorMiddleware {
val middleware: Middleware<AppState> = { _, appStateProvider ->
{ next ->
{ action ->
val appState = appStateProvider()
if (action is WalletSelectorAction && appState != null) {
handleAction(action, appState.walletSelectorState)
}
next(action)
}
}
}
private fun handleAction(action: WalletSelectorAction, state: WalletSelectorState) {
when (action) {
is WalletSelectorAction.UserWalletsLoaded -> {
fetchWalletStores(action.userWallets)
}
is WalletSelectorAction.WalletStoresChanged -> {
updateBalances(action.walletsStores, state)
}
is WalletSelectorAction.UnlockWithBiometry -> {
unlockWallets()
}
is WalletSelectorAction.AddWallet -> {
addWallet()
}
is WalletSelectorAction.SelectWallet -> {
selectWallet(action.userWalletId)
}
is WalletSelectorAction.RemoveWallets -> {
deleteWallets(action.userWalletsIds, state)
}
is WalletSelectorAction.RenameWallet -> {
renameWallet(action.userWalletId, action.newName)
}
is WalletSelectorAction.ChangeAppCurrency -> {
refreshUserWalletsAmounts()
}
is WalletSelectorAction.ClearUserWallets -> {
clearUserWalletsAndCloseError()
}
is WalletSelectorAction.AddWallet.Success,
is WalletSelectorAction.AddWallet.Error,
is WalletSelectorAction.SelectedWalletChanged,
is WalletSelectorAction.UnlockWithBiometry.Error,
is WalletSelectorAction.UnlockWithBiometry.Success,
is WalletSelectorAction.BalancesLoaded,
is WalletSelectorAction.IsLockedChanged,
is WalletSelectorAction.CloseError,
-> Unit
}
}
private fun fetchWalletStores(userWallets: List<UserWallet>) {
scope.launch {
walletStoresManager.fetch(userWallets)
.doOnFailure { error ->
Timber.e(error, "Unable to fetch wallet stores")
}
}
}
private fun updateBalances(
updatedWalletStores: Map<UserWalletId, List<WalletStoreModel>>,
state: WalletSelectorState,
) {
if (updatedWalletStores.isNotEmpty()) {
scope.launch(Dispatchers.Default) {
val updatedWallets = state.wallets.calculateBalanceAndUpdateWalletStores(updatedWalletStores)
if (updatedWallets != state.wallets) {
store.dispatchOnMain(WalletSelectorAction.BalancesLoaded(updatedWallets))
}
}
}
}
private fun unlockWallets() {
Analytics.send(MyWallets.Button.UnlockWithBiometrics())
scope.launch {
userWalletsListManager.unlockIfLockable()
.doOnFailure { error ->
Timber.e(error, "Unable to unlock all user wallets")
store.dispatchOnMain(WalletSelectorAction.UnlockWithBiometry.Error(error))
}
.doOnSuccess {
store.dispatchOnMain(WalletSelectorAction.UnlockWithBiometry.Success)
}
}
}
private fun addWallet() = scope.launch {
Analytics.send(MyWallets.Button.ScanNewCard())
val cardSdkConfigRepository = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository)
val prevUseBiometricsForAccessCode = cardSdkConfigRepository.isBiometricsRequestPolicy()
// Update access code policy for access code saving when a card was scanned
cardSdkConfigRepository.setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes,
)
store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan(
analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.MyWallets),
onWalletNotCreated = {
// No need to rollback policy, continue with the policy set before the card scan
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
},
disclaimerWillShow = {
store.dispatchOnMain(NavigationAction.PopBackTo())
},
onSuccess = { scanResponse ->
saveUserWalletAndPopBackToWalletScreen(scanResponse)
.doOnFailure { error ->
// Rollback policy if card saving was failed
cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode)
Timber.e(error, "Unable to save user wallet")
store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error))
}
},
onFailure = { error ->
// Rollback policy if card scanning was failed
cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode)
Timber.e(error, "Unable to scan card")
store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error))
},
)
}
private suspend fun saveUserWalletAndPopBackToWalletScreen(scanResponse: ScanResponse): CompletionResult<Unit> {
val userWallet = UserWalletBuilder(scanResponse).build()
?: return CompletionResult.Failure(TangemSdkError.WalletIsNotCreated())
return userWalletsListManager.save(userWallet)
.doOnSuccess {
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(userWallet)
}
}
private fun selectWallet(userWalletId: UserWalletId) {
scope.launch {
userWalletsListManager.get(userWalletId)
.flatMap { userWallet ->
if (userWallet.isLocked) {
unlockUserWalletWithScannedCard(userWallet)
} else {
userWalletsListManager.select(userWalletId)
}
}
.doOnFailure { error ->
Timber.e(
error,
"""
Unable to select user wallet
|- Wallet ID to select: $userWalletId
""".trimIndent(),
)
}
.doOnSuccess {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(
userWallet = selectedUserWallet,
sendAnalyticsEvent = true,
)
}
}
}
}
private suspend fun unlockUserWalletWithScannedCard(userWallet: UserWallet): CompletionResult<Unit> {
Analytics.send(MyWallets.Button.WalletUnlockTapped())
tangemSdkManager.changeDisplayedCardIdNumbersCount(userWallet.scanResponse)
return store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan()
.map { scanResponse ->
val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
if (scannedUserWalletId == userWallet.walletId) {
userWallet.updateCardWallets(scanResponse)
} else {
// TODO: Display error
Timber.e(
"""
Unable to unlock and select user wallet
|- Excepted ID: ${userWallet.walletId}
|- Received ID: $scannedUserWalletId
""".trimIndent(),
)
error("Wrong card")
}
}
.flatMap { updatedUserWallet ->
userWalletsListManager.save(updatedUserWallet, canOverride = true)
}
.doOnFailure {
tangemSdkManager.changeDisplayedCardIdNumbersCount(
scanResponse = userWalletsListManager.selectedUserWalletSync?.scanResponse,
)
}
}
private fun UserWallet.updateCardWallets(scanResponse: ScanResponse): UserWallet {
return this.copy(
scanResponse = this.scanResponse.copy(
card = this.scanResponse.card.copy(
wallets = scanResponse.card.wallets,
),
),
)
}
private fun deleteWallets(userWalletsIds: List<UserWalletId>, state: WalletSelectorState) {
Analytics.send(MyWallets.Button.DeleteWalletTapped())
scope.launch {
when (userWalletsIds.size) {
state.wallets.size -> clearUserWalletsAndPopBackToHomeScreen()
else -> deleteUserWallets(
userWalletsIds = userWalletsIds,
currentSelectedWalletId = state.selectedWalletId,
)
}
.doOnFailure { error ->
Timber.e(
error,
"""
Unable to delete user wallets
|- Wallets IDs to delete: $userWalletsIds
|- Current wallets IDs: ${state.wallets.map { it.id }}
""".trimIndent(),
)
}
}
}
private fun renameWallet(userWalletId: UserWalletId, newName: String) {
scope.launch {
userWalletsListManager.update(userWalletId) { it.copy(name = newName) }
.doOnFailure { error ->
Timber.e(
error,
"""
Unable to rename user wallet
|- Wallet ID: $userWalletId
|- New name: $newName
""".trimIndent(),
)
}
}
}
private fun refreshUserWalletsAmounts() {
val featureToggles = store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles)
if (featureToggles.isRedesignedScreenEnabled) return
scope.launch {
walletStoresManager.updateAmounts(
userWallets = userWalletsListManager.userWallets.firstOrNull().orEmpty(),
)
.doOnFailure { error ->
Timber.e(error, "Unable to refresh user wallets amounts")
}
}
}
private fun clearUserWalletsAndCloseError() = scope.launch {
clearUserWallets()
.doOnSuccess {
store.dispatchWithMain(WalletSelectorAction.CloseError)
popBackToHome()
}
.doOnFailure { e ->
Timber.e(e, "Unable to clear user wallets")
}
}
private suspend fun clearUserWalletsAndPopBackToHomeScreen(): CompletionResult<Unit> {
return clearUserWallets()
.doOnSuccess { popBackToHome() }
}
private suspend fun clearUserWallets(): CompletionResult<Unit> {
return userWalletsListManager.clear()
.flatMap { walletStoresManager.clear() }
.flatMap { tangemSdkManager.clearSavedUserCodes() }
}
private suspend fun popBackToHome() {
// !!! Workaround !!!
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet))
delay(timeMillis = 280)
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home))
}
private suspend fun deleteUserWallets(
userWalletsIds: List<UserWalletId>,
currentSelectedWalletId: UserWalletId?,
): CompletionResult<Unit> {
return userWalletsListManager.delete(userWalletsIds)
.flatMap { walletStoresManager.delete(userWalletsIds) }
.flatMap { deleteAccessCodes(userWalletsIds) }
.doOnSuccess {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
when {
selectedUserWallet == null -> {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
}
currentSelectedWalletId != selectedUserWallet.walletId -> {
store.onUserWalletSelected(
userWallet = selectedUserWallet,
sendAnalyticsEvent = true,
)
}
}
}
}
private suspend fun deleteAccessCodes(userWalletsIds: List<UserWalletId>): CompletionResult<Unit> {
val cardsIds = userWalletsListManager.userWallets.firstOrNull()
?.asSequence()
?.filter { it.walletId in userWalletsIds }
?.flatMap { it.cardsInWallet }
?.toSet()
return if (cardsIds.isNullOrEmpty()) {
CompletionResult.Success(Unit)
} else {
tangemSdkManager.deleteSavedUserCodes(cardsIds.toSet())
}
}
private suspend fun List<UserWalletModel>.calculateBalanceAndUpdateWalletStores(
walletStores: Map<UserWalletId, List<WalletStoreModel>>,
): List<UserWalletModel> {
return this
.associateWith { walletStores[it.id] }
.map { (wallet, walletStores) ->
wallet.calculateBalanceAndUpdateWalletStores(walletStores)
}
}
private suspend fun UserWalletModel.calculateBalanceAndUpdateWalletStores(
walletStores: List<WalletStoreModel>?,
): UserWalletModel {
return this.copy(
type = when (type) {
is UserWalletModel.Type.MultiCurrency -> type.copy(
tokensCount = walletStores?.flatMap { it.walletsData }?.size ?: 0,
)
is UserWalletModel.Type.SingleCurrency -> type
},
fiatBalance = totalFiatBalanceCalculator.calculate(
walletStores = walletStores.orEmpty(),
initial = TotalFiatBalance.Loading,
),
)
}
}

View file

@ -1,106 +0,0 @@
package com.tangem.tap.features.walletSelector.redux
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.model.TotalFiatBalance
import org.rekotlin.Action
internal object WalletSelectorReducer {
fun reduce(action: Action, state: AppState): WalletSelectorState {
return if (action is WalletSelectorAction) {
internalReduce(action, state.walletSelectorState)
} else {
state.walletSelectorState
}
}
@Suppress("ComplexMethod")
private fun internalReduce(action: WalletSelectorAction, state: WalletSelectorState): WalletSelectorState {
return when (action) {
is WalletSelectorAction.UserWalletsLoaded -> state.copy(
wallets = action.userWallets.updateWalletsModels(state.wallets),
)
is WalletSelectorAction.SelectedWalletChanged -> state.copy(
selectedWalletId = action.selectedWallet.walletId,
)
is WalletSelectorAction.IsLockedChanged -> state.copy(
isLocked = action.isLocked,
)
is WalletSelectorAction.BalancesLoaded -> state.copy(
wallets = action.userWalletModels,
)
is WalletSelectorAction.CloseError -> state.copy(error = null)
is WalletSelectorAction.UnlockWithBiometry -> state.copy(
isUnlockInProgress = true,
)
is WalletSelectorAction.UnlockWithBiometry.Error -> state.copy(
isUnlockInProgress = false,
error = action.error,
)
is WalletSelectorAction.UnlockWithBiometry.Success -> state.copy(
isUnlockInProgress = false,
)
is WalletSelectorAction.AddWallet -> state.copy(
isCardSavingInProgress = true,
)
is WalletSelectorAction.AddWallet.Error -> state.copy(
isCardSavingInProgress = false,
error = action.error,
)
is WalletSelectorAction.AddWallet.Success -> state.copy(
isCardSavingInProgress = false,
)
is WalletSelectorAction.ChangeAppCurrency -> state.copy(
fiatCurrency = action.fiatCurrency,
)
is WalletSelectorAction.WalletStoresChanged,
is WalletSelectorAction.SelectWallet,
is WalletSelectorAction.RemoveWallets,
is WalletSelectorAction.RenameWallet,
is WalletSelectorAction.ClearUserWallets,
-> state
}
}
private fun List<UserWallet>.updateWalletsModels(prevWallets: List<UserWalletModel>): List<UserWalletModel> {
return this.map { userWallet ->
prevWallets
.find { it.id == userWallet.walletId }
?.let {
it.copy(
name = userWallet.name,
artworkUrl = userWallet.artworkUrl,
isLocked = userWallet.isLocked,
type = userWallet.getType(prevType = it.type),
)
}
?: with(userWallet) {
UserWalletModel(
id = walletId,
name = name,
artworkUrl = artworkUrl,
type = getType(),
fiatBalance = TotalFiatBalance.Loading,
isLocked = isLocked,
)
}
}
}
private fun UserWallet.getType(prevType: UserWalletModel.Type? = null): UserWalletModel.Type {
return if (isMultiCurrency) {
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(
blockchainName = scanResponse.cardTypesResolver.getBlockchain().fullName,
)
}
}
}

View file

@ -1,16 +0,0 @@
package com.tangem.tap.features.walletSelector.redux
import com.tangem.common.core.TangemError
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.common.entities.FiatCurrency
import org.rekotlin.StateType
data class WalletSelectorState(
val wallets: List<UserWalletModel> = emptyList(),
val selectedWalletId: UserWalletId? = null,
val isLocked: Boolean = false,
val fiatCurrency: FiatCurrency = FiatCurrency.Default,
val isCardSavingInProgress: Boolean = false,
val isUnlockInProgress: Boolean = false,
val error: TangemError? = null,
) : StateType

View file

@ -1,43 +0,0 @@
package com.tangem.tap.features.walletSelector.ui
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.toFormattedFiatValue
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.features.walletSelector.redux.UserWalletModel
import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem
internal fun List<UserWalletModel>.toUiModels(appCurrency: FiatCurrency): Sequence<UserWalletItem> {
return this.asSequence().map { userWalletModel ->
with(userWalletModel) {
val balance = when (fiatBalance) {
is TotalFiatBalance.Failed -> UserWalletItem.Balance.Failed
is TotalFiatBalance.Loading -> UserWalletItem.Balance.Loading
is TotalFiatBalance.Loaded -> UserWalletItem.Balance.Loaded(
amount = fiatBalance.amount.toFormattedFiatValue(appCurrency.symbol, appCurrency.code),
showWarning = fiatBalance.isWarning,
)
}
when (type) {
is UserWalletModel.Type.MultiCurrency -> MultiCurrencyUserWalletItem(
id = id,
name = name,
imageUrl = artworkUrl,
balance = balance,
isLocked = isLocked,
cardsInWallet = type.cardsInWallet,
tokensCount = type.tokensCount,
)
is UserWalletModel.Type.SingleCurrency -> SingleCurrencyUserWalletItem(
id = id,
name = name,
imageUrl = artworkUrl,
balance = balance,
isLocked = isLocked,
tokenName = type.blockchainName,
)
}
}
}
}

View file

@ -1,100 +0,0 @@
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
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.components.wallets.RenameWalletDialogContent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeBottomSheetFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
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.*
import com.tangem.tap.features.walletSelector.ui.model.DialogModel
import com.tangem.tap.features.walletSelector.ui.model.WarningModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
private val viewModel by viewModels<WalletSelectorViewModel>()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
Analytics.send(MyWallets.MyWalletsScreenOpened())
return super.onCreateDialog(savedInstanceState)
}
@Composable
override fun ScreenContent(modifier: Modifier) {
val state by viewModel.state.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
val dialog by rememberUpdatedState(newValue = state.dialog)
Box(modifier = modifier.nestedScroll(rememberNestedScrollInteropConnection())) {
WalletSelectorScreenContent(
state = state,
onWalletClick = viewModel::walletClicked,
onWalletLongClick = viewModel::walletLongClicked,
onUnlockClick = viewModel::unlock,
onAddCardClick = viewModel::addWallet,
onClearSelectedClick = viewModel::cancelWalletsEditing,
onEditSelectedWalletClick = viewModel::renameWallet,
onDeleteSelectedWalletsClick = viewModel::deleteWallets,
)
SnackbarHost(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(vertical = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
hostState = snackbarHostState,
)
}
Dialog(dialog = dialog)
LaunchedEffect(key1 = errorMessage) {
errorMessage?.let {
snackbarHostState.showSnackbar(it)
viewModel.closeError()
}
}
}
@Suppress("TopLevelComposableFunctions")
@Composable
private fun Dialog(dialog: DialogModel?) {
if (dialog == null) return
when (dialog) {
is DialogModel.RemoveWalletDialog -> RemoveWalletDialogContent(dialog)
is DialogModel.RenameWalletDialog -> {
RenameWalletDialogContent(
name = dialog.currentName,
onConfirm = dialog.onConfirm,
onDismiss = dialog.onDismiss,
)
}
is WarningModel.BiometricsLockoutWarning -> BiometricsLockoutWarningContent(dialog)
is WarningModel.KeyInvalidatedWarning -> KeyInvalidatedWarningContent(dialog)
is WarningModel.BiometricsDisabledWarning -> BiometricsDisabledWarningContent(dialog)
}
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.tap.features.walletSelector.ui
import androidx.compose.runtime.Immutable
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.walletSelector.ui.model.DialogModel
import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem
@Immutable
internal data class WalletSelectorScreenState(
val multiCurrencyWallets: List<MultiCurrencyUserWalletItem> = emptyList(),
val singleCurrencyWallets: List<SingleCurrencyUserWalletItem> = emptyList(),
val selectedUserWalletId: UserWalletId? = null,
val isLocked: Boolean = false,
val editingUserWalletsIds: List<UserWalletId> = listOf(),
val dialog: DialogModel? = null,
val showAddCardProgress: Boolean = false,
val showUnlockProgress: Boolean = false,
val error: TextReference? = null,
)

View file

@ -1,278 +0,0 @@
package com.tangem.tap.features.walletSelector.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.common.core.TangemError
import com.tangem.core.analytics.Analytics
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.isLocked
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.common.analytics.events.MyWallets
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction
import com.tangem.tap.features.walletSelector.redux.WalletSelectorState
import com.tangem.tap.features.walletSelector.ui.model.DialogModel
import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.WarningModel
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletStoresManager
import kotlinx.coroutines.flow.*
import org.rekotlin.StoreSubscriber
internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSelectorState> {
private val stateInternal = MutableStateFlow(WalletSelectorScreenState())
val state: StateFlow<WalletSelectorScreenState> = stateInternal
init {
subscribeToStoreChanges()
bootstrapWalletListChanges()
bootstrapWalletsStoresChanges()
bootstrapAppFiatCurrency()
}
fun unlock() {
store.dispatch(WalletSelectorAction.UnlockWithBiometry)
}
fun addWallet() {
store.dispatch(WalletSelectorAction.AddWallet)
}
fun walletClicked(userWalletId: UserWalletId) = with(state.value) {
when {
editingUserWalletsIds.isNotEmpty() && !editingUserWalletsIds.contains(userWalletId) -> {
editWallet(userWalletId)
}
editingUserWalletsIds.isNotEmpty() && editingUserWalletsIds.contains(userWalletId) -> {
cancelWalletEditing(userWalletId)
}
selectedUserWalletId != userWalletId -> {
store.dispatch(
WalletSelectorAction.SelectWallet(
userWalletId = userWalletId,
sendAnalyticsEvent = true,
),
)
}
}
}
fun walletLongClicked(userWalletId: UserWalletId) = with(state.value) {
if (editingUserWalletsIds.isEmpty()) {
editWallet(userWalletId)
}
}
fun cancelWalletsEditing() {
stateInternal.update { prevState ->
prevState.copy(
editingUserWalletsIds = emptyList(),
)
}
}
fun renameWallet() = with(state.value) {
if (editingUserWalletsIds.isNotEmpty() && dialog == null) {
val editedUserWalletId = editingUserWalletsIds.first()
val editedUserWallet = (multiCurrencyWallets + singleCurrencyWallets)
.find { it.id == editedUserWalletId }
if (editedUserWallet != null) {
Analytics.send(MyWallets.Button.EditWalletTapped())
val dialog = DialogModel.RenameWalletDialog(
currentName = editedUserWallet.name,
onConfirm = { newName ->
store.dispatch(WalletSelectorAction.RenameWallet(editedUserWalletId, newName))
stateInternal.update { prevState ->
prevState.copy(
dialog = null,
editingUserWalletsIds = emptyList(),
)
}
},
onDismiss = {
stateInternal.update { prevState ->
prevState.copy(
dialog = null,
)
}
},
)
stateInternal.update { prevState ->
prevState.copy(
dialog = dialog,
)
}
}
}
}
fun deleteWallets() = with(state.value) {
if (editingUserWalletsIds.isNotEmpty()) {
val dialog = DialogModel.RemoveWalletDialog(
onConfirm = {
store.dispatch(WalletSelectorAction.RemoveWallets(editingUserWalletsIds))
stateInternal.update { prevState ->
prevState.copy(
dialog = null,
editingUserWalletsIds = emptyList(),
)
}
},
onDismiss = {
stateInternal.update { prevState ->
prevState.copy(
dialog = null,
)
}
},
)
stateInternal.update { prevState ->
prevState.copy(
dialog = dialog,
)
}
}
}
fun closeError() {
store.dispatch(WalletSelectorAction.CloseError)
}
// TODO: Refactor errors handling
override fun newState(state: WalletSelectorState) {
stateInternal.update { prevState ->
val walletsUi = state.wallets.toUiModels(state.fiatCurrency)
val walletsIds = walletsUi.map { it.id }
val multiCurrencyWallets = arrayListOf<MultiCurrencyUserWalletItem>()
val singleCurrencyWallets = arrayListOf<SingleCurrencyUserWalletItem>()
walletsUi.forEach { wallet ->
when (wallet) {
is MultiCurrencyUserWalletItem -> {
multiCurrencyWallets.add(wallet)
}
is SingleCurrencyUserWalletItem -> {
singleCurrencyWallets.add(wallet)
}
}
}
val warningDialog = createWarningDialogIfNeeded(state.error, prevState.dialog)
prevState.copy(
multiCurrencyWallets = multiCurrencyWallets,
singleCurrencyWallets = singleCurrencyWallets,
selectedUserWalletId = state.selectedWalletId,
editingUserWalletsIds = prevState.editingUserWalletsIds.filter { it in walletsIds },
isLocked = state.isLocked,
showUnlockProgress = state.isUnlockInProgress,
showAddCardProgress = state.isCardSavingInProgress,
dialog = warningDialog,
error = state.error
?.takeIf { !it.silent && warningDialog == null }
?.let { error ->
error.messageResId?.let { TextReference.Res(it) }
?: TextReference.Str(error.customMessage)
},
)
}
}
override fun onCleared() {
store.unsubscribe(this)
}
private fun createWarningDialogIfNeeded(error: TangemError?, currentDialog: DialogModel?): DialogModel? {
return when (error) {
is UserWalletsListError.BiometricsAuthenticationLockout -> WarningModel.BiometricsLockoutWarning(
isPermanent = error.isPermanent,
onDismiss = this::dismissWarningDialog,
)
is UserWalletsListError.AllKeysInvalidated,
is UserWalletsListError.NoUserWalletSelected,
-> WarningModel.KeyInvalidatedWarning(
onDismiss = this::dismissWarningDialog,
)
is UserWalletsListError.BiometricsAuthenticationDisabled -> WarningModel.BiometricsDisabledWarning(
onDismiss = this::clearUserWallets,
)
else -> currentDialog
}
}
private fun clearUserWallets() {
store.dispatch(WalletSelectorAction.ClearUserWallets)
}
private fun dismissWarningDialog() {
stateInternal.update { prevState ->
prevState.copy(
dialog = null,
)
}
closeError()
}
private fun editWallet(userWalletId: UserWalletId) {
stateInternal.update { prevState ->
prevState.copy(
editingUserWalletsIds = prevState.editingUserWalletsIds + userWalletId,
)
}
}
private fun cancelWalletEditing(userWalletId: UserWalletId) {
stateInternal.update { prevState ->
prevState.copy(
editingUserWalletsIds = prevState.editingUserWalletsIds - userWalletId,
)
}
}
private fun subscribeToStoreChanges() {
store.subscribe(this) { appState ->
appState.skip { old, new -> old.walletSelectorState == new.walletSelectorState }
.select { it.walletSelectorState }
}
}
private fun bootstrapWalletListChanges() {
userWalletsListManager.userWallets
.onEach {
store.dispatchOnMain(WalletSelectorAction.UserWalletsLoaded(userWallets = it))
}
.launchIn(viewModelScope)
userWalletsListManager.selectedUserWallet
.onEach {
store.dispatchOnMain(WalletSelectorAction.SelectedWalletChanged(selectedWallet = it))
}
.launchIn(viewModelScope)
userWalletsListManager.isLocked
.onEach {
store.dispatchOnMain(WalletSelectorAction.IsLockedChanged(isLocked = it))
}
.launchIn(viewModelScope)
}
private fun bootstrapWalletsStoresChanges() {
walletStoresManager.getAll()
.onEach { walletStores ->
store.dispatchOnMain(WalletSelectorAction.WalletStoresChanged(walletStores))
}
.launchIn(viewModelScope)
}
private fun bootstrapAppFiatCurrency() {
store.dispatch(
WalletSelectorAction.ChangeAppCurrency(
fiatCurrency = store.state.globalState.appCurrency,
),
)
}
}

View file

@ -1,55 +0,0 @@
package com.tangem.tap.features.walletSelector.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.walletSelector.ui.model.WarningModel
import com.tangem.wallet.R
@Composable
internal fun BiometricsDisabledWarningContent(warning: WarningModel.BiometricsDisabledWarning) {
BasicDialog(
title = stringResource(id = R.string.common_warning),
message = stringResource(id = R.string.biometric_unavailable_warning),
onDismissDialog = warning.onDismiss,
isDismissable = false,
confirmButton = DialogButton(
title = stringResource(id = R.string.common_ok),
onClick = warning.onDismiss,
),
)
}
// region Preview
@Composable
private fun BiometricsDisabledWarningContentSample(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
BiometricsDisabledWarningContent(
warning = WarningModel.BiometricsDisabledWarning(
onDismiss = {},
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun BiometricsDisabledWarningContentPreview_Light() {
TangemTheme {
BiometricsDisabledWarningContentSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun BiometricsDisabledWarningContentPreview_Dark() {
TangemTheme(isDark = true) {
BiometricsDisabledWarningContentSample()
}
}
// endregion Preview

View file

@ -1,89 +0,0 @@
package com.tangem.tap.features.walletSelector.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.walletSelector.ui.model.WarningModel
import com.tangem.wallet.R
@Composable
internal fun BiometricsLockoutWarningContent(warning: WarningModel.BiometricsLockoutWarning) {
BasicDialog(
title = stringResource(id = R.string.biometric_lockout_warning_title),
message = stringResource(
id = if (warning.isPermanent) {
R.string.biometric_lockout_permanent_warning_description
} else {
R.string.biometric_lockout_warning_description
},
),
onDismissDialog = warning.onDismiss,
confirmButton = DialogButton(
title = stringResource(id = R.string.common_ok),
onClick = warning.onDismiss,
),
)
}
// region Preview
@Composable
private fun BiometricsLockoutDialogSample(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
BiometricsLockoutWarningContent(
warning = WarningModel.BiometricsLockoutWarning(
isPermanent = false,
onDismiss = {},
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun BiometricsLockoutDialogPreview_Light() {
TangemTheme {
BiometricsLockoutDialogSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun BiometricsLockoutDialogPreview_Dark() {
TangemTheme(isDark = true) {
BiometricsLockoutDialogSample()
}
}
@Composable
private fun BiometricsLockoutDialog_Permanent_Sample(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
BiometricsLockoutWarningContent(
warning = WarningModel.BiometricsLockoutWarning(
isPermanent = true,
onDismiss = {},
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun BiometricsLockoutDialog_Permanent_Preview_Light() {
TangemTheme {
BiometricsLockoutDialog_Permanent_Sample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun BiometricsLockoutDialog_Permanent_Preview_Dark() {
TangemTheme(isDark = true) {
BiometricsLockoutDialog_Permanent_Sample()
}
}
// endregion Preview

View file

@ -1,52 +0,0 @@
package com.tangem.tap.features.walletSelector.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.walletSelector.ui.model.WarningModel
import com.tangem.wallet.R
@Composable
internal fun KeyInvalidatedWarningContent(warning: WarningModel.KeyInvalidatedWarning) {
BasicDialog(
title = stringResource(id = R.string.common_attention),
message = stringResource(id = R.string.key_invalidated_warning_description),
onDismissDialog = warning.onDismiss,
confirmButton = DialogButton(
title = stringResource(id = R.string.common_ok),
onClick = warning.onDismiss,
),
)
}
// region Preview
@Composable
private fun KeyInvalidatedWarningSample(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
KeyInvalidatedWarningContent(
warning = WarningModel.KeyInvalidatedWarning(onDismiss = {}),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun KeyInvalidatedWarningPreview_Light() {
TangemTheme {
KeyInvalidatedWarningSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun KeyInvalidatedWarningPreview_Dark() {
TangemTheme(isDark = true) {
KeyInvalidatedWarningSample()
}
}
// endregion Preview

View file

@ -1,45 +0,0 @@
package com.tangem.tap.features.walletSelector.ui.components
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.features.walletSelector.ui.WalletSelectorScreenState
import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem
internal object MockData {
private val multiCurrencyUserWallet = MultiCurrencyUserWalletItem(
id = UserWalletId("wallet_1"),
balance = UserWalletItem.Balance.Loaded(
amount = "6781.05 $",
showWarning = true,
),
name = "Wallet",
imageUrl = "https://app.tangem.com/cards/card_default.png",
isLocked = false,
tokensCount = 12,
cardsInWallet = 3,
)
private val singleCurrencyUserWallet = SingleCurrencyUserWalletItem(
id = UserWalletId("wallet_4"),
balance = UserWalletItem.Balance.Loaded(
amount = "6781.05 $",
showWarning = false,
),
name = "Wallet",
imageUrl = "https://app.tangem.com/cards/card_default.png",
isLocked = false,
tokenName = "Ethereum",
)
val state = WalletSelectorScreenState(
multiCurrencyWallets = listOf(
multiCurrencyUserWallet,
multiCurrencyUserWallet.copy(id = UserWalletId("wallet_2")),
multiCurrencyUserWallet.copy(id = UserWalletId("wallet_3"), tokensCount = 2, cardsInWallet = 1),
),
singleCurrencyWallets = listOf(singleCurrencyUserWallet),
selectedUserWalletId = multiCurrencyUserWallet.id,
isLocked = false,
)
}

View file

@ -1,24 +0,0 @@
package com.tangem.tap.features.walletSelector.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.tap.features.walletSelector.ui.model.DialogModel
import com.tangem.wallet.R
@Composable
internal fun RemoveWalletDialogContent(dialog: DialogModel.RemoveWalletDialog) {
BasicDialog(
message = stringResource(id = R.string.user_wallet_list_delete_prompt),
confirmButton = DialogButton(
title = stringResource(id = R.string.common_delete),
warning = true,
onClick = dialog.onConfirm,
),
dismissButton = DialogButton(
onClick = dialog.onDismiss,
),
onDismissDialog = dialog.onDismiss,
)
}

View file

@ -1,330 +0,0 @@
package com.tangem.tap.features.walletSelector.ui.components
import androidx.annotation.StringRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButtonIconEnd
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.features.walletSelector.ui.WalletSelectorScreenState
import com.tangem.wallet.R
@Suppress("LongParameterList")
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun WalletSelectorScreenContent(
state: WalletSelectorScreenState,
onWalletClick: (UserWalletId) -> Unit,
onWalletLongClick: (UserWalletId) -> Unit,
onUnlockClick: () -> Unit,
onAddCardClick: () -> Unit,
onClearSelectedClick: () -> Unit,
onEditSelectedWalletClick: () -> Unit,
onDeleteSelectedWalletsClick: () -> Unit,
) {
LazyColumn {
stickyHeader {
Header(
editingWalletsIds = state.editingUserWalletsIds,
onClearSelectedClick = onClearSelectedClick,
onEditSelectedWalletClick = onEditSelectedWalletClick,
onDeleteSelectedWalletsClick = onDeleteSelectedWalletsClick,
)
}
item {
WalletsTitle(textResId = R.string.user_wallet_list_multi_header, wallets = state.multiCurrencyWallets)
}
itemsIndexed(
items = state.multiCurrencyWallets,
key = { _, wallet -> wallet.id.stringValue },
) { _, wallet ->
WalletItem(
wallet = wallet,
isSelected = remember(state.selectedUserWalletId) { wallet.id == state.selectedUserWalletId },
isChecked = remember(state.editingUserWalletsIds) { wallet.id in state.editingUserWalletsIds },
onWalletClick = { onWalletClick(wallet.id) },
onWalletLongClick = { onWalletLongClick(wallet.id) },
)
}
item {
WalletsTitle(textResId = R.string.user_wallet_list_single_header, wallets = state.singleCurrencyWallets)
}
itemsIndexed(
items = state.singleCurrencyWallets,
key = { _, wallet -> wallet.id.stringValue },
) { _, wallet ->
WalletItem(
wallet = wallet,
isSelected = remember(state.selectedUserWalletId) { wallet.id == state.selectedUserWalletId },
isChecked = remember(state.editingUserWalletsIds) { wallet.id in state.editingUserWalletsIds },
onWalletClick = { onWalletClick(wallet.id) },
onWalletLongClick = { onWalletLongClick(wallet.id) },
)
}
item {
Footer(
isLocked = state.isLocked,
showUnlockProgress = state.showUnlockProgress,
showAddCardProgress = state.showAddCardProgress,
onUnlockClick = onUnlockClick,
onAddCardClick = onAddCardClick,
)
}
}
}
@Composable
private fun Header(
editingWalletsIds: List<UserWalletId>,
onClearSelectedClick: () -> Unit,
onEditSelectedWalletClick: () -> Unit,
onDeleteSelectedWalletsClick: () -> Unit,
) {
val editingWalletsSize by rememberUpdatedState(newValue = editingWalletsIds.size)
val hasEditingWallets by remember { derivedStateOf { editingWalletsSize > 0 } }
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = TangemTheme.colors.background.plain,
shape = TangemTheme.shapes.bottomSheet,
),
) {
Hand()
Box(
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size44),
contentAlignment = Alignment.Center,
) {
if (hasEditingWallets) {
EditWalletsBar(
editingWalletsSize = editingWalletsSize,
onClearSelectedClick = onClearSelectedClick,
onEditSelectedWalletClick = onEditSelectedWalletClick,
onDeleteSelectedWalletsClick = onDeleteSelectedWalletsClick,
)
} else {
Text(
text = stringResource(R.string.user_wallet_list_title),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
}
}
}
}
@Composable
private fun WalletsTitle(@StringRes textResId: Int, wallets: List<*>) {
if (wallets.isNotEmpty()) {
Text(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing16),
text = stringResource(id = textResId),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
}
}
@Composable
private fun Footer(
isLocked: Boolean,
showUnlockProgress: Boolean,
showAddCardProgress: Boolean,
onUnlockClick: () -> Unit,
onAddCardClick: () -> Unit,
) {
Column(
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing24,
bottom = TangemTheme.dimens.spacing16,
)
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
if (isLocked) {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(
id = R.string.user_wallet_list_unlock_all_with,
stringResource(id = R.string.common_biometrics),
),
showProgress = showUnlockProgress,
onClick = onUnlockClick,
)
}
SecondaryButtonIconEnd(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.user_wallet_list_add_button),
showProgress = showAddCardProgress,
iconResId = R.drawable.ic_tangem_24,
onClick = onAddCardClick,
)
}
}
@Composable
private fun EditWalletsBar(
editingWalletsSize: Int,
onClearSelectedClick: () -> Unit,
onEditSelectedWalletClick: () -> Unit,
onDeleteSelectedWalletsClick: () -> Unit,
) {
val showEditAction by remember(editingWalletsSize) {
derivedStateOf { editingWalletsSize in 1 until 2 }
}
Row(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
IconButton(
modifier = Modifier.size(TangemTheme.dimens.size32),
onClick = onClearSelectedClick,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_close_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = "Unselect wallets",
)
}
Text(
text = stringResource(id = R.string.user_wallet_list_editing_count, editingWalletsSize),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
SpacerWMax()
if (showEditAction) {
IconButton(
modifier = Modifier.size(TangemTheme.dimens.size32),
onClick = onEditSelectedWalletClick,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_pencil_outline_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = "Change wallet name",
)
}
}
IconButton(
modifier = Modifier.size(TangemTheme.dimens.size32),
onClick = onDeleteSelectedWalletsClick,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_trash),
tint = TangemTheme.colors.icon.warning,
contentDescription = "Delete selected wallets",
)
}
}
}
// region Preview
@Composable
private fun WalletSelectorScreenContentSample(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(color = TangemTheme.colors.background.primary),
) {
WalletSelectorScreenContent(
state = MockData.state.copy(isLocked = true),
onWalletClick = { /* no-op */ },
onWalletLongClick = { /* no-op */ },
onUnlockClick = { /* no-op */ },
onAddCardClick = { /* no-op */ },
onEditSelectedWalletClick = { /* no-op */ },
onClearSelectedClick = { /* no-op */ },
onDeleteSelectedWalletsClick = { /* no-op */ },
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun WalletSelectorScreenContentPreview_Light() {
TangemTheme {
WalletSelectorScreenContentSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun WalletSelectorScreenContentPreview_Dark() {
TangemTheme(isDark = true) {
WalletSelectorScreenContentSample()
}
}
@Composable
private fun WalletSelectorScreenContent_EditWallets_Sample(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary),
) {
WalletSelectorScreenContent(
state = MockData.state
.copy(editingUserWalletsIds = listOf(MockData.state.multiCurrencyWallets[2].id)),
onWalletClick = { /* no-op */ },
onWalletLongClick = { /* no-op */ },
onUnlockClick = { /* no-op */ },
onAddCardClick = { /* no-op */ },
onEditSelectedWalletClick = { /* no-op */ },
onClearSelectedClick = { /* no-op */ },
onDeleteSelectedWalletsClick = { /* no-op */ },
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun WalletSelectorScreenContent_EditWallets_Preview_Light() {
TangemTheme {
WalletSelectorScreenContent_EditWallets_Sample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun WalletSelectorScreenContent_EditWallets_Preview_Dark() {
TangemTheme(isDark = true) {
WalletSelectorScreenContent_EditWallets_Sample()
}
}
// endregion Preview

View file

@ -1,459 +0,0 @@
package com.tangem.tap.features.walletSelector.ui.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.SpacerH2
import com.tangem.core.ui.components.SpacerW6
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.common.extensions.cardImageData
import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem
import com.tangem.wallet.R
import com.valentinilk.shimmer.shimmer
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun WalletItem(
wallet: UserWalletItem,
isSelected: Boolean,
isChecked: Boolean,
onWalletClick: () -> Unit,
onWalletLongClick: () -> Unit,
) {
Row(
modifier = Modifier
.combinedClickable(
onClick = onWalletClick,
onLongClick = onWalletLongClick,
)
.heightIn(min = TangemTheme.dimens.size72)
.padding(all = TangemTheme.dimens.spacing16),
verticalAlignment = Alignment.CenterVertically,
) {
WalletCardImage(
cardImageUrl = wallet.imageUrl,
isChecked = isChecked,
isSelected = isSelected,
)
SpacerW8()
WalletInfo(wallet = wallet, isSelected = isSelected)
SpacerW6()
TokensInfo(
isLocked = wallet.isLocked,
balance = wallet.balance,
tokensCount = (wallet as? MultiCurrencyUserWalletItem)?.tokensCount,
)
}
}
@Composable
private fun WalletCardImage(cardImageUrl: String, isChecked: Boolean, isSelected: Boolean) {
Box(
modifier = Modifier
.width(TangemTheme.dimens.size62)
.height(TangemTheme.dimens.size42),
) {
val cardImageModifier = Modifier
.align(Alignment.Center)
.width(TangemTheme.dimens.size56)
.height(TangemTheme.dimens.size32)
.clip(TangemTheme.shapes.roundedCornersSmall2)
val tintColor = TangemTheme.colors.icon.accent.copy(alpha = .6f)
val checkedColorFilter = remember(isChecked) {
if (isChecked) {
ColorFilter.tint(
color = tintColor,
blendMode = BlendMode.SrcOver,
)
} else {
null
}
}
SubcomposeAsyncImage(
modifier = cardImageModifier,
model = ImageRequest.Builder(LocalContext.current)
.cardImageData(cardImageUrl)
.crossfade(true)
.build(),
loading = { CardImageShimmer(modifier = cardImageModifier) },
error = { CardImagePlaceholder(modifier = cardImageModifier) },
colorFilter = checkedColorFilter,
contentDescription = null,
)
when {
isChecked -> {
CheckedWalletMark(
modifier = Modifier.matchParentSize(),
)
}
isSelected -> {
SelectedWalletBadge(
modifier = Modifier.align(Alignment.TopEnd),
)
}
}
}
}
@Composable
private fun RowScope.WalletInfo(wallet: UserWalletItem, isSelected: Boolean) {
Column(
modifier = Modifier.weight(weight = 2f),
verticalArrangement = Arrangement.SpaceAround,
horizontalAlignment = Alignment.Start,
) {
Text(
text = wallet.name,
color = if (isSelected) TangemTheme.colors.text.accent else TangemTheme.colors.text.primary1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography.subtitle1,
)
SpacerH2()
Text(
text = when (wallet) {
is MultiCurrencyUserWalletItem -> pluralStringResource(
id = R.plurals.card_label_card_count,
count = wallet.cardsInWallet,
wallet.cardsInWallet,
)
is SingleCurrencyUserWalletItem -> wallet.tokenName
},
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
style = TangemTheme.typography.caption2,
)
}
}
@Composable
private fun RowScope.TokensInfo(isLocked: Boolean, balance: UserWalletItem.Balance, tokensCount: Int?) {
Column(
modifier = Modifier.weight(weight = 3f),
verticalArrangement = Arrangement.SpaceAround,
horizontalAlignment = Alignment.End,
) {
if (isLocked) {
LockedPlaceholder()
} else {
when (balance) {
is UserWalletItem.Balance.Failed,
is UserWalletItem.Balance.Loaded,
-> {
LoadedTokensInfo(
balanceAmount = balance.amount,
tokensCount = tokensCount,
showWarning = balance.showWarning,
)
}
is UserWalletItem.Balance.Loading -> {
LoadingTokensInfo(isMultiCurrencyWallet = tokensCount != null)
}
}
}
}
}
@Composable
private fun LoadingTokensInfo(isMultiCurrencyWallet: Boolean) {
Column(
modifier = Modifier.shimmer(),
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.SpaceAround,
) {
Box(
modifier = Modifier
.width(TangemTheme.dimens.size62)
.height(TangemTheme.dimens.size16)
.background(
color = TangemTheme.colors.button.secondary,
shape = TangemTheme.shapes.roundedCornersSmall2,
),
)
if (isMultiCurrencyWallet) {
SpacerH2()
Box(
modifier = Modifier
.width(TangemTheme.dimens.size48)
.height(TangemTheme.dimens.size12)
.background(
color = TangemTheme.colors.button.secondary,
shape = TangemTheme.shapes.roundedCornersSmall2,
),
)
}
}
}
@Composable
private fun LoadedTokensInfo(
balanceAmount: String,
tokensCount: Int?,
showWarning: Boolean,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.End,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
) {
Text(
text = balanceAmount,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.End,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography.subtitle1,
)
if (showWarning) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size16),
painter = painterResource(id = R.drawable.ic_alert_24),
tint = TangemTheme.colors.icon.attention,
contentDescription = null,
)
}
}
if (tokensCount != null) {
SpacerH2()
Text(
text = pluralStringResource(
id = R.plurals.token_count,
count = tokensCount,
tokensCount,
),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.End,
)
}
}
}
@Composable
private fun LockedPlaceholder(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.padding(vertical = TangemTheme.dimens.spacing2)
.background(
color = TangemTheme.colors.button.secondary,
shape = TangemTheme.shapes.roundedCornersMedium,
),
) {
Icon(
modifier = Modifier
.padding(
vertical = TangemTheme.dimens.spacing8,
horizontal = TangemTheme.dimens.spacing12,
)
.size(TangemTheme.dimens.size20),
painter = painterResource(id = R.drawable.ic_locked_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
}
}
@Composable
private fun CardImagePlaceholder(modifier: Modifier = Modifier) {
Box(modifier = modifier) {
Image(
modifier = Modifier.matchParentSize(),
painter = painterResource(id = R.drawable.card_placeholder_black),
contentDescription = null,
)
}
}
@Composable
private fun CardImageShimmer(modifier: Modifier = Modifier) {
Box(modifier = modifier.shimmer()) {
Box(
modifier = Modifier
.matchParentSize()
.background(TangemTheme.colors.button.secondary),
)
}
}
@Composable
private fun SelectedWalletBadge(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(TangemTheme.dimens.size18)
.background(
color = Color.White,
shape = CircleShape,
),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.padding(all = 0.5.dp),
painter = painterResource(id = R.drawable.ic_check_circle_18),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
}
}
@Composable
private fun CheckedWalletMark(modifier: Modifier = Modifier) {
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_check_24),
tint = TangemTheme.colors.icon.primary2,
contentDescription = null,
)
}
}
// region Preview
@Composable
@Suppress("LongMethod") // preview
private fun WalletItemSample(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary),
) {
WalletItem(
wallet = MultiCurrencyUserWalletItem(
id = UserWalletId(value = null),
name = "Tangem Card",
imageUrl = "",
balance = UserWalletItem.Balance.Loaded(
amount = "1412121218 BTC",
showWarning = false,
),
isLocked = false,
tokensCount = 2,
cardsInWallet = 1,
),
isSelected = false,
isChecked = false,
onWalletClick = {},
onWalletLongClick = {},
)
Divider(modifier = Modifier.padding(vertical = 4.dp))
WalletItem(
wallet = MultiCurrencyUserWalletItem(
id = UserWalletId(value = null),
name = "Tangem Card",
imageUrl = "",
balance = UserWalletItem.Balance.Loaded(
amount = "1412121218 BTC",
showWarning = true,
),
isLocked = false,
tokensCount = 2,
cardsInWallet = 1,
),
isSelected = false,
isChecked = false,
onWalletClick = {},
onWalletLongClick = {},
)
Divider(modifier = Modifier.padding(vertical = 4.dp))
WalletItem(
wallet = MultiCurrencyUserWalletItem(
id = UserWalletId(value = null),
name = "Tangem Card",
imageUrl = "",
balance = UserWalletItem.Balance.Loading,
isLocked = true,
tokensCount = 2,
cardsInWallet = 1,
),
isSelected = false,
isChecked = false,
onWalletClick = {},
onWalletLongClick = {},
)
Divider(modifier = Modifier.padding(vertical = 4.dp))
WalletItem(
wallet = MultiCurrencyUserWalletItem(
id = UserWalletId(value = null),
name = "Tangem Card",
imageUrl = "",
balance = UserWalletItem.Balance.Failed,
isLocked = false,
tokensCount = 2,
cardsInWallet = 1,
),
isSelected = true,
isChecked = false,
onWalletClick = {},
onWalletLongClick = {},
)
Divider(modifier = Modifier.padding(vertical = 4.dp))
WalletItem(
wallet = SingleCurrencyUserWalletItem(
id = UserWalletId(value = null),
name = "Tangem Card",
imageUrl = "",
balance = UserWalletItem.Balance.Loaded(
amount = "141212121888 BTC",
showWarning = false,
),
isLocked = false,
tokenName = "Bitcoin",
),
isSelected = false,
isChecked = true,
onWalletClick = {},
onWalletLongClick = {},
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun WalletItemPreview_Light() {
TangemTheme {
WalletItemSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun WalletItemPreview_Dark() {
TangemTheme(isDark = true) {
WalletItemSample()
}
}
// endregion Preview

View file

@ -1,29 +0,0 @@
package com.tangem.tap.features.walletSelector.ui.model
internal sealed interface DialogModel {
data class RenameWalletDialog(
val currentName: String,
val onConfirm: (newName: String) -> Unit,
val onDismiss: () -> Unit,
) : DialogModel
data class RemoveWalletDialog(
val onConfirm: () -> Unit,
val onDismiss: () -> Unit,
) : DialogModel
}
internal sealed interface WarningModel : DialogModel {
data class BiometricsLockoutWarning(
val isPermanent: Boolean,
val onDismiss: () -> Unit,
) : WarningModel
data class KeyInvalidatedWarning(
val onDismiss: () -> Unit,
) : WarningModel
data class BiometricsDisabledWarning(
val onDismiss: () -> Unit,
) : WarningModel
}

View file

@ -1,47 +0,0 @@
package com.tangem.tap.features.walletSelector.ui.model
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.wallets.models.UserWalletId
internal sealed interface UserWalletItem {
val id: UserWalletId
val name: String
val imageUrl: String
val balance: Balance
val isLocked: Boolean
sealed class Balance {
open val amount: String = BigDecimalFormatter.EMPTY_BALANCE_SIGN
open val showWarning: Boolean = false
object Loading : Balance()
object Failed : Balance() {
override val showWarning: Boolean = true
}
data class Loaded(
override val amount: String,
override val showWarning: Boolean,
) : Balance()
}
}
internal data class MultiCurrencyUserWalletItem(
override val id: UserWalletId,
override val name: String,
override val imageUrl: String,
override val balance: UserWalletItem.Balance,
override val isLocked: Boolean,
val tokensCount: Int,
val cardsInWallet: Int,
) : UserWalletItem
internal data class SingleCurrencyUserWalletItem(
override val id: UserWalletId,
override val name: String,
override val imageUrl: String,
override val balance: UserWalletItem.Balance,
override val isLocked: Boolean,
val tokenName: String,
) : UserWalletItem

View file

@ -34,7 +34,6 @@ enum class AppScreen(val isDialogFragment: Boolean = false) {
Swap,
Welcome,
SaveWallet(isDialogFragment = true),
WalletSelector(isDialogFragment = true),
AppCurrencySelector,
ModalNotification(isDialogFragment = true),
}