Updated on 2026-08-14

This commit is contained in:
Tangem 2022-12-01 14:40:31 +03:00
parent d3e3f5cfd5
commit a0f1a6591c
28 changed files with 905 additions and 44 deletions

View file

@ -37,6 +37,10 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
import com.tangem.tap.domain.totalBalance.di.provideDefaultImplementation
import com.tangem.tap.domain.walletStores.WalletStoresManager
import com.tangem.tap.domain.walletStores.di.provideDummyImplementation
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.persistence.CardBalanceStateAdapter
@ -60,6 +64,14 @@ lateinit var shopService: TangemShopService
lateinit var assetReader: AssetReader
lateinit var userTokensRepository: UserTokensRepository
val walletStoresManager by lazy {
WalletStoresManager.provideDummyImplementation()
}
val totalFiatBalanceCalculator by lazy {
TotalFiatBalanceCalculator.provideDefaultImplementation()
}
@HiltAndroidApp
class TapApplication : Application(), ImageLoaderFactory {

View file

@ -16,6 +16,7 @@ import com.tangem.tap.features.shop.redux.ShopReducer
import com.tangem.tap.features.tokens.redux.TokensReducer
import com.tangem.tap.features.wallet.redux.reducers.WalletReducer
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.features.walletSelector.redux.WalletSelectorReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
import org.rekotlin.Action
@ -40,6 +41,7 @@ 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),
)
}

View file

@ -35,6 +35,7 @@ import com.tangem.tap.features.tokens.redux.TokensMiddleware
import com.tangem.tap.features.tokens.redux.TokensState
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.middlewares.WalletMiddleware
import com.tangem.tap.features.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
@ -88,6 +89,7 @@ data class AppState(
ShopMiddleware().shopMiddleware,
WelcomeMiddleware().middleware,
SaveWalletMiddleware().middleware,
WalletSelectorMiddleware().middleware,
)
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.domain.model
import java.math.BigDecimal
/**
* Represents fiat balance of [WalletStoreModel] list
* @property amount Amount of the total balance
* */
sealed class TotalFiatBalance {
open val amount: BigDecimal = BigDecimal.ZERO
object Loading : TotalFiatBalance()
class Refreshing(
override val amount: BigDecimal,
) : TotalFiatBalance()
class Error(
override val amount: BigDecimal,
) : TotalFiatBalance()
class Loaded(
override val amount: BigDecimal,
) : TotalFiatBalance()
}

View file

@ -2,9 +2,17 @@ package com.tangem.tap.domain.model
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.util.UserWalletId
import com.tangem.domain.common.util.userWalletId
import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl
/**
* Represents user's wallet which stored in app persistence
* @param name User's wallet name
* @param walletId User's wallet [UserWalletId]
* @param artworkUrl User wallet card artwork URL
* @param cardsInWallet List of cards IDs assigned with this user's wallet
* @param scanResponse [ScanResponse] of primary user's wallet card.
* TODO: Replace with [com.tangem.domain.common.CardDTO]
* @property cardId ID of user's wallet primary card
* */
data class UserWallet(
val name: String,
val walletId: UserWalletId,
@ -14,24 +22,4 @@ data class UserWallet(
) {
val cardId: String
get() = scanResponse.card.cardId
val hasAccessCode: Boolean
get() = scanResponse.card.isAccessCodeSet
companion object {
suspend operator fun invoke(
scanResponse: ScanResponse,
backupCardsIds: Set<String>? = null,
): UserWallet {
return with(scanResponse) {
UserWallet(
walletId = card.userWalletId,
name = productType.name,
artworkUrl = card.getOrLoadCardArtworkUrl(),
cardsInWallet = backupCardsIds?.plus(card.cardId) ?: setOf(card.cardId),
scanResponse = this,
)
}
}
}
}

View file

@ -0,0 +1,79 @@
package com.tangem.tap.domain.model
import com.tangem.tap.domain.model.WalletDataModel.Status
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.redux.AddressData
import java.math.BigDecimal
/**
* Contains info about wallet store currency
* @param currency Wallet's [Currency]
* @param status Wallet's [Status], represents current status of that currency
* @param walletAddresses List of wallet data [AddressData]
* @param existentialDeposit Amount that must be held on currency's balance, if balance is below that amount all
* founds will be destroyed. Null if currency don't have existential deposit
* @param fiatRate Wallet's fiat rate, used to calculate fiat balance. Null if not provided
* */
data class WalletDataModel(
val currency: Currency,
val status: Status,
val walletAddresses: List<AddressData>,
val existentialDeposit: BigDecimal?,
val fiatRate: BigDecimal?,
) {
/**
* Represent current status of currency
* @property amount Currency amount
* @property pendingTransactions List of currency [PendingTransaction] sent in currency's blockchain
* @property errorMessage Status error message, null if not provided
* @property isErrorStatus true if current status is error status, false otherwise
* */
sealed class Status {
open val amount: BigDecimal = BigDecimal.ZERO
open val pendingTransactions: List<PendingTransaction> = emptyList()
open val errorMessage: String? = null
open val isErrorStatus: Boolean = false
}
object Loading : Status()
data class VerifiedOnline(
override val amount: BigDecimal,
) : Status()
data class TransactionInProgress(
override val amount: BigDecimal,
override val pendingTransactions: List<PendingTransaction>,
) : Status()
data class SameCurrencyTransactionInProgress(
override val amount: BigDecimal,
override val pendingTransactions: List<PendingTransaction>,
) : Status()
data class NoAccount(
val amountToCreateAccount: BigDecimal?,
) : Status() {
override val isErrorStatus: Boolean = true
}
data class Unreachable(
override val errorMessage: String?,
) : Status() {
override val isErrorStatus: Boolean = true
}
object MissedDerivation : Status() {
override val isErrorStatus: Boolean = true
}
data class Refreshing(
override val amount: BigDecimal,
override val pendingTransactions: List<PendingTransaction>,
override val errorMessage: String?,
) : Status() {
override val isErrorStatus: Boolean = errorMessage != null
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.tap.domain.model
import com.tangem.blockchain.common.WalletManager
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.domain.model.WalletStoreModel.WalletRent
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import java.math.BigDecimal
/**
* Contains info about the blockchain and its currencies
* @param userWalletId ID of the [UserWallet] which uses that store
* @param blockchainNetwork Store's [BlockchainNetwork]
* @param walletManager Store's [WalletManager], may be null if it fails to create this manager
* @param walletsData List of [WalletDataModel] which represents store's blockchain currency and tokens currencies
* @param walletRent Store's [WalletRent], null if store has no rent or currency balance is greater then
* [WalletRent.exemptionAmount]
* */
data class WalletStoreModel(
val userWalletId: UserWalletId,
val blockchainNetwork: BlockchainNetwork,
val walletManager: WalletManager?,
val walletsData: List<WalletDataModel>,
val walletRent: WalletRent?,
) {
/**
* Represents wallet blockchain rent
* @param rent Amount that will be charged in overtime if the blockchain does not have an amount greater than
* the [WalletRent.exemptionAmount]
* @param exemptionAmount Amount that should be on the blockchain balance not to pay rent
* */
data class WalletRent(
val rent: BigDecimal,
val exemptionAmount: BigDecimal,
)
}

View file

@ -0,0 +1,30 @@
package com.tangem.tap.domain.model.builders
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.util.userWalletId
import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl
import com.tangem.tap.domain.model.UserWallet
class UserWalletBuilder(
private val scanResponse: ScanResponse,
) {
private var backupCardsIds: Set<String> = emptySet()
fun setBackupCardsIds(backupCardsIds: Set<String>?) = this.apply {
if (backupCardsIds != null) {
this.backupCardsIds = backupCardsIds
}
}
suspend fun build(): UserWallet {
return with(scanResponse) {
UserWallet(
walletId = card.userWalletId,
name = productType.name,
artworkUrl = card.getOrLoadCardArtworkUrl(),
cardsInWallet = backupCardsIds.plus(card.cardId),
scanResponse = this,
)
}
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.tap.domain.totalBalance
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.domain.model.WalletStoreModel
import java.math.BigDecimal
interface TotalFiatBalanceCalculator {
suspend fun calculate(
prevAmount: BigDecimal,
walletStores: List<WalletStoreModel>,
): TotalFiatBalance
companion object
}

View file

@ -0,0 +1,8 @@
package com.tangem.tap.domain.totalBalance.di
import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
import com.tangem.tap.domain.totalBalance.implementation.DefaultTotalFiatBalanceCalculator
fun TotalFiatBalanceCalculator.Companion.provideDefaultImplementation(): TotalFiatBalanceCalculator {
return DefaultTotalFiatBalanceCalculator()
}

View file

@ -0,0 +1,102 @@
package com.tangem.tap.domain.totalBalance.implementation
import com.tangem.tap.common.extensions.toFiatValue
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigDecimal
internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator {
override suspend fun calculate(
prevAmount: BigDecimal,
walletStores: List<WalletStoreModel>,
): TotalFiatBalance {
return if (walletStores.isEmpty()) {
TotalFiatBalance.Loading
} else {
withContext(Dispatchers.Default) {
val walletsData = walletStores
.asSequence()
.flatMap { it.walletsData }
val calculateAmount = { walletsData.calculateTotalFiatAmount() }
when (walletsData.findStatus()) {
TotalFiatBalanceStatus.Loading -> TotalFiatBalance.Loading
TotalFiatBalanceStatus.Refreshing -> TotalFiatBalance.Refreshing(prevAmount)
TotalFiatBalanceStatus.Error -> TotalFiatBalance.Error(calculateAmount())
TotalFiatBalanceStatus.Loaded -> TotalFiatBalance.Loaded(calculateAmount())
}
}
}
}
private fun Sequence<WalletDataModel>.findStatus(): TotalFiatBalanceStatus {
return this
.mapToStatus()
.reduce { prevStatus, newStatus ->
getCurrentStatus(prevStatus, newStatus)
}
}
private fun Sequence<WalletDataModel>.mapToStatus(): Sequence<TotalFiatBalanceStatus> {
return this.map { walletData ->
when (walletData.status) {
is WalletDataModel.Refreshing -> TotalFiatBalanceStatus.Refreshing
is WalletDataModel.VerifiedOnline,
is WalletDataModel.SameCurrencyTransactionInProgress,
is WalletDataModel.TransactionInProgress,
is WalletDataModel.NoAccount,
-> TotalFiatBalanceStatus.Loaded
is WalletDataModel.Unreachable,
is WalletDataModel.MissedDerivation,
-> TotalFiatBalanceStatus.Error
is WalletDataModel.Loading -> TotalFiatBalanceStatus.Loading
}
}
}
private fun Sequence<WalletDataModel>.calculateTotalFiatAmount(): BigDecimal {
return this
.map { walletData ->
walletData.fiatRate
?.let { walletData.status.amount.toFiatValue(it) }
?: BigDecimal.ZERO
}
.reduce(BigDecimal::plus)
}
private fun getCurrentStatus(
prevStatus: TotalFiatBalanceStatus,
newStatus: TotalFiatBalanceStatus,
): TotalFiatBalanceStatus {
return when (prevStatus) {
TotalFiatBalanceStatus.Loading -> prevStatus
TotalFiatBalanceStatus.Refreshing -> when (newStatus) {
TotalFiatBalanceStatus.Loading -> prevStatus
TotalFiatBalanceStatus.Refreshing,
TotalFiatBalanceStatus.Error,
TotalFiatBalanceStatus.Loaded,
-> newStatus
}
TotalFiatBalanceStatus.Loaded,
TotalFiatBalanceStatus.Error,
-> when (newStatus) {
TotalFiatBalanceStatus.Loading,
TotalFiatBalanceStatus.Refreshing,
TotalFiatBalanceStatus.Error,
-> newStatus
TotalFiatBalanceStatus.Loaded -> prevStatus
}
}
}
private enum class TotalFiatBalanceStatus {
Loading,
Refreshing,
Error,
Loaded,
}
}

View file

@ -25,5 +25,7 @@ interface UserWalletsListManager {
suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit>
suspend fun clear(): CompletionResult<Unit>
suspend fun get(walletId: UserWalletId): CompletionResult<UserWallet>
companion object
}

View file

@ -55,4 +55,10 @@ class DummyUserWalletsListManager : UserWalletsListManager {
override suspend fun clear(): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
override suspend fun get(walletId: UserWalletId): CompletionResult<UserWallet> {
return catching {
error("Not implemented")
}
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.tap.domain.walletStores
import com.tangem.common.CompletionResult
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.model.WalletStoreModel
import kotlinx.coroutines.flow.Flow
interface WalletStoresManager {
fun getAll(): Flow<Map<UserWalletId, List<WalletStoreModel>>>
fun get(userWalletId: UserWalletId): Flow<List<WalletStoreModel>>
suspend fun delete(userWalletsIds: List<String>): CompletionResult<Unit>
suspend fun clear(): CompletionResult<Unit>
suspend fun fetch(
userWallet: UserWallet,
refresh: Boolean = false,
): CompletionResult<Unit>
suspend fun fetch(
userWallets: List<UserWallet>,
refresh: Boolean = false,
): CompletionResult<Unit>
companion object
}

View file

@ -0,0 +1,8 @@
package com.tangem.tap.domain.walletStores.di
import com.tangem.tap.domain.walletStores.WalletStoresManager
import com.tangem.tap.domain.walletStores.implementation.DummyWalletStoresManager
fun WalletStoresManager.Companion.provideDummyImplementation(): WalletStoresManager {
return DummyWalletStoresManager()
}

View file

@ -0,0 +1,35 @@
package com.tangem.tap.domain.walletStores.implementation
import com.tangem.common.CompletionResult
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.walletStores.WalletStoresManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
internal class DummyWalletStoresManager : WalletStoresManager {
override fun getAll(): Flow<Map<UserWalletId, List<WalletStoreModel>>> {
return emptyFlow()
}
override fun get(userWalletId: UserWalletId): Flow<List<WalletStoreModel>> {
return emptyFlow()
}
override suspend fun delete(userWalletsIds: List<String>): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
override suspend fun clear(): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
override suspend fun fetch(userWallet: UserWallet, refresh: Boolean): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
override suspend fun fetch(userWallets: List<UserWallet>, refresh: Boolean): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
}

View file

@ -4,12 +4,12 @@ import com.tangem.common.CompletionResult
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.flatMap
import com.tangem.common.map
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
@ -65,10 +65,9 @@ internal class SaveWalletMiddleware {
?: return
scope.launch {
val userWallet = UserWallet(
scanResponse = scanResponse,
backupCardsIds = state.additionalInfo?.backupCardsIds,
)
val userWallet = UserWalletBuilder(scanResponse)
.setBackupCardsIds(backupCardsIds = state.additionalInfo?.backupCardsIds)
.build()
userWalletsListManager.save(userWallet)
.flatMap {
@ -108,14 +107,6 @@ internal class SaveWalletMiddleware {
cardsIds = userWallet.cardsInWallet,
)
}
userWallet.hasAccessCode -> {
tangemSdkManager
.scanCard(
cardId = userWallet.cardId,
useBiometricsForAccessCode = true,
)
.map { /* no-op */ }
}
else -> {
CompletionResult.Success(Unit)
}

View file

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

View file

@ -1,9 +1,33 @@
package com.tangem.tap.features.walletSelector.redux
import com.tangem.common.core.TangemError
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.domain.model.UserWallet
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 BalanceLoaded(
val userWalletModel: UserWalletModel,
) : WalletSelectorAction
object UnlockWithBiometry : WalletSelectorAction {
object Success : WalletSelectorAction
data class Error(val error: TangemError) : WalletSelectorAction
@ -27,5 +51,10 @@ internal sealed interface WalletSelectorAction : Action {
data class Error(val error: TangemError) : WalletSelectorAction
}
data class ChangeAppCurrency(
val fiatCurrency: FiatCurrency,
) : WalletSelectorAction
data class HandleError(val error: TangemError) : WalletSelectorAction
object CloseError : WalletSelectorAction
}

View file

@ -0,0 +1,212 @@
package com.tangem.tap.features.walletSelector.redux
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
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.*
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
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 -> {
unlockWalletsWithBiometry()
}
is WalletSelectorAction.AddWallet -> {
addWallet()
}
is WalletSelectorAction.SelectWallet -> {
selectWallet(action.walletId)
}
is WalletSelectorAction.RemoveWallets -> {
removeWallets(action.walletIdsToRemove, state)
}
is WalletSelectorAction.RenameWallet -> {
renameWallet(action.walletId, action.newName)
}
is WalletSelectorAction.ChangeAppCurrency,
is WalletSelectorAction.AddWallet.Success,
is WalletSelectorAction.AddWallet.Error,
is WalletSelectorAction.SelectedWalletChanged,
is WalletSelectorAction.UnlockWithBiometry.Error,
is WalletSelectorAction.UnlockWithBiometry.Success,
is WalletSelectorAction.BalanceLoaded,
is WalletSelectorAction.IsLockedChanged,
is WalletSelectorAction.HandleError,
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")
store.dispatchOnMain(WalletSelectorAction.HandleError(error))
}
}
}
private fun updateBalances(walletStores: Map<UserWalletId, List<WalletStoreModel>>, state: WalletSelectorState) {
walletStores.forEach { (walletId, walletStores) ->
scope.launch {
val updatedWallet = state.wallets
.find { it.id == walletId.stringValue }
?.let { wallet ->
wallet.copy(
type = when (val type = wallet.type) {
is UserWalletModel.Type.MultiCurrency -> type.copy(
tokensCount = walletStores.flatMap { it.walletsData }.size,
)
is UserWalletModel.Type.SingleCurrency -> type.copy(
blockchainName = walletStores
.firstOrNull()
?.blockchainNetwork
?.blockchain
?.fullName,
)
},
fiatBalance = totalFiatBalanceCalculator.calculate(
prevAmount = wallet.fiatBalance.amount,
walletStores = walletStores,
),
)
}
if (updatedWallet != null) {
store.dispatchOnMain(WalletSelectorAction.BalanceLoaded(updatedWallet))
}
}
}
}
private fun unlockWalletsWithBiometry() {
scope.launch {
userWalletsListManager.unlockWithBiometry()
.doOnFailure { error ->
store.dispatchOnMain(WalletSelectorAction.UnlockWithBiometry.Error(error))
}
.doOnSuccess {
store.dispatchOnMain(WalletSelectorAction.UnlockWithBiometry.Success)
}
}
}
private fun addWallet() = scope.launch {
scanCardInternal { scanResponse ->
val userWallet = UserWalletBuilder(scanResponse).build()
userWalletsListManager.save(userWallet)
.doOnFailure { error ->
store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error))
}
.doOnSuccess {
val selectedWallet = userWalletsListManager.selectedUserWallet.first()
val isSavedWalletSelected = userWallet == selectedWallet
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
if (isSavedWalletSelected) {
store.dispatchOnMain(NavigationAction.PopBackTo())
store.onUserWalletSelected(selectedWallet)
}
}
}
}
private fun selectWallet(id: String) {
scope.launch {
userWalletsListManager.selectWallet(UserWalletId(id))
.doOnFailure { error ->
store.dispatchOnMain(WalletSelectorAction.HandleError(error))
}
.doOnSuccess { selectedWallet ->
store.dispatchOnMain(NavigationAction.PopBackTo())
store.onUserWalletSelected(selectedWallet)
}
}
}
private fun removeWallets(walletIdsToRemove: List<String>, state: WalletSelectorState) {
scope.launch {
when (walletIdsToRemove.size) {
state.wallets.size -> { // Removing all wallets
userWalletsListManager.clear()
.flatMap { walletStoresManager.clear() }
.doOnSuccess {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
}
else -> {
userWalletsListManager.delete(walletIdsToRemove.map { UserWalletId(it) })
.flatMap { walletStoresManager.delete(walletIdsToRemove) }
}
}
.doOnFailure { error ->
store.dispatchOnMain(WalletSelectorAction.HandleError(error))
}
}
}
private fun renameWallet(walletId: String, newName: String) {
scope.launch {
userWalletsListManager.get(walletId = UserWalletId(walletId))
.map { it.copy(name = newName) }
.flatMap { userWalletsListManager.update(it) }
.doOnFailure { error ->
store.dispatchOnMain(WalletSelectorAction.HandleError(error))
}
}
}
private suspend inline fun scanCardInternal(
crossinline onCardScanned: suspend (ScanResponse) -> Unit,
) {
ScanCardProcessor.scan(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,
onSuccess = {
onCardScanned(it)
},
onFailure = { error ->
store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error))
},
onWalletNotCreated = {
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
store.dispatchOnMain(NavigationAction.PopBackTo())
},
)
}
}

View file

@ -0,0 +1,107 @@
package com.tangem.tap.features.walletSelector.redux
import com.tangem.domain.common.CardDTO
import com.tangem.tap.common.extensions.replaceByOrAdd
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.domain.model.UserWallet
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
}
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.stringValue,
)
is WalletSelectorAction.IsLockedChanged -> state.copy(
isLocked = action.isLocked,
)
is WalletSelectorAction.BalanceLoaded -> state.copy(
wallets = state.wallets.updateWithBalance(action.userWalletModel),
)
is WalletSelectorAction.HandleError -> state.copy(error = action.error)
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,
-> state
}
}
private fun List<UserWallet>.updateWalletsModels(prevWallets: List<UserWalletModel>): List<UserWalletModel> {
return this.map { userWallet ->
prevWallets
.find { it.id == userWallet.walletId.stringValue }
?.copy(
name = userWallet.name,
artworkUrl = userWallet.artworkUrl,
)
?: with(userWallet) {
UserWalletModel(
id = walletId.stringValue,
name = name,
artworkUrl = artworkUrl,
type = getType(),
fiatBalance = TotalFiatBalance.Loading,
)
}
}
}
private fun List<UserWalletModel>.updateWithBalance(
userWalletModel: UserWalletModel,
): List<UserWalletModel> {
return ArrayList(this).apply {
replaceByOrAdd(userWalletModel) { it.id == userWalletModel.id }
}
}
private fun UserWallet.getType(): UserWalletModel.Type {
return if (scanResponse.card.isMultiwalletAllowed) {
UserWalletModel.Type.MultiCurrency(
tokensCount = 0,
cardsInWallet = (scanResponse.card.backupStatus as? CardDTO.BackupStatus.Active)
?.cardCount?.inc()
?: 1,
)
} else {
UserWalletModel.Type.SingleCurrency(
blockchainName = null,
)
}
}
}

View file

@ -1,5 +1,15 @@
package com.tangem.tap.features.walletSelector.redux
import com.tangem.common.core.TangemError
import com.tangem.tap.common.entities.FiatCurrency
import org.rekotlin.StateType
class WalletSelectorState : StateType
data class WalletSelectorState(
val wallets: List<UserWalletModel> = emptyList(),
val selectedWalletId: String? = 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

@ -0,0 +1,76 @@
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.details.ui.cardsettings.TextReference
import com.tangem.tap.features.walletSelector.redux.UserWalletModel
import com.tangem.tap.features.walletSelector.redux.WalletSelectorState
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 WalletSelectorScreenState.updateWithNewState(
newState: WalletSelectorState,
): WalletSelectorScreenState {
val walletsUi = newState.wallets.toUiModels(newState.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)
}
}
}
return this.copy(
multiCurrencyWallets = multiCurrencyWallets,
singleCurrencyWallets = singleCurrencyWallets,
selectedWalletId = newState.selectedWalletId,
editingWalletsIds = editingWalletsIds.filter { it in walletsIds },
isLocked = newState.isLocked,
showUnlockProgress = newState.isUnlockInProgress,
showAddCardProgress = newState.isCardSavingInProgress,
error = newState.error
?.takeUnless { it.silent }
?.let { error ->
error.messageResId?.let { TextReference.Res(it) }
?: TextReference.Str(error.customMessage)
},
)
}
private fun List<UserWalletModel>.toUiModels(
appCurrency: FiatCurrency,
): Sequence<UserWalletItem> {
return this.asSequence().map { userWalletModel ->
with(userWalletModel) {
val balance = UserWalletItem.Balance(
amount = fiatBalance.amount.toFormattedFiatValue(appCurrency.symbol),
isLoading = fiatBalance is TotalFiatBalance.Loading,
)
when (type) {
is UserWalletModel.Type.MultiCurrency -> MultiCurrencyUserWalletItem(
id = id,
name = name,
imageUrl = artworkUrl,
balance = balance,
cardsInWallet = type.cardsInWallet,
tokensCount = type.tokensCount,
)
is UserWalletModel.Type.SingleCurrency -> SingleCurrencyUserWalletItem(
id = id,
name = name,
imageUrl = artworkUrl,
balance = balance,
tokenName = type.blockchainName ?: "",
)
}
}
}
}

View file

@ -14,6 +14,7 @@ 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.features.details.ui.cardsettings.resolveReference
import com.tangem.tap.features.walletSelector.ui.components.RenameWalletDialogContent
import com.tangem.tap.features.walletSelector.ui.components.WalletSelectorScreenContent
import com.tangem.tap.features.walletSelector.ui.model.RenameWalletDialog
@ -33,7 +34,7 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<Wa
state: WalletSelectorScreenState,
) {
val snackbarHostState = remember { SnackbarHostState() }
val errorMessage by rememberUpdatedState(newValue = state.error?.customMessage)
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
val renameWalletDialog by rememberUpdatedState(newValue = state.renameWalletDialog)
Box(

View file

@ -1,7 +1,7 @@
package com.tangem.tap.features.walletSelector.ui
import androidx.compose.runtime.Immutable
import com.tangem.common.core.TangemError
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.RenameWalletDialog
import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem
@ -16,5 +16,5 @@ internal data class WalletSelectorScreenState(
val renameWalletDialog: RenameWalletDialog? = null,
val showAddCardProgress: Boolean = false,
val showUnlockProgress: Boolean = false,
val error: TangemError? = null,
val error: TextReference? = null,
)

View file

@ -1,12 +1,18 @@
package com.tangem.tap.features.walletSelector.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction
import com.tangem.tap.features.walletSelector.redux.WalletSelectorState
import com.tangem.tap.features.walletSelector.ui.model.RenameWalletDialog
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletStoresManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import org.rekotlin.StoreSubscriber
@ -18,6 +24,7 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
subscribeToStoreChanges()
bootstrapWalletListChanges()
bootstrapWalletsStoresChanges()
bootstrapAppFiatCurrency()
}
fun unlock() {
@ -104,7 +111,9 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
}
override fun newState(state: WalletSelectorState) {
// TODO: Update UI state
stateInternal.update { prevState ->
prevState.updateWithNewState(state)
}
}
override fun onCleared() {
@ -135,10 +144,38 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
}
private fun bootstrapWalletListChanges() {
// TODO
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() {
// TODO
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

@ -8,7 +8,7 @@ import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.scope
import com.tangem.tap.store
@ -58,7 +58,7 @@ internal class WelcomeMiddleware {
private fun proceedWithCard() = scope.launch {
scanCardInternal { scanResponse ->
val userWallet = UserWallet(scanResponse)
val userWallet = UserWalletBuilder(scanResponse).build()
userWalletsListManager.unlockWithCard(userWallet)
.doOnFailure { error ->