Updated on 2026-08-14

This commit is contained in:
Tangem 2023-02-28 15:10:20 +03:00
commit 9f46037b4c
53 changed files with 860 additions and 377 deletions

View file

@ -40,10 +40,9 @@ import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Context) {
@ -68,6 +67,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
cardId: String? = null,
additionalBlockchainsToDerive: Collection<Blockchain>? = null,
messageRes: Int? = null,
allowsRequestAccessCodeFromRepository: Boolean = false,
): CompletionResult<ScanResponse> {
val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header))
return runTaskAsyncReturnOnMain(
@ -75,6 +75,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
card = null,
userTokensRepository = userTokensRepository,
additionalBlockchainsToDerive = additionalBlockchainsToDerive,
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
),
cardId = cardId,
initialMessage = message,
@ -111,7 +112,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
}
suspend fun derivePublicKeys(
cardId: String,
cardId: String?,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
): CompletionResult<DerivationTaskResponse> {
return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId)
@ -195,9 +196,9 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
accessCode: String? = null,
): CompletionResult<T> =
withContext(Dispatchers.Main) {
suspendCoroutine { continuation ->
suspendCancellableCoroutine { continuation ->
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage, accessCode) { result ->
if (continuation.context.isActive) continuation.resume(result)
if (continuation.isActive) continuation.resume(result)
}
}
}

View file

@ -7,8 +7,8 @@ import com.tangem.blockchain.common.Wallet
import com.tangem.common.CompletionResult
import com.tangem.domain.common.CardDTO
import com.tangem.tap.domain.tasks.SignHashesTask
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class TangemSigner(
private val card: CardDTO,
@ -20,9 +20,9 @@ class TangemSigner(
override suspend fun sign(
hashes: List<ByteArray>,
publicKey: Wallet.PublicKey
publicKey: Wallet.PublicKey,
): CompletionResult<List<ByteArray>> {
return suspendCoroutine { continuation ->
return suspendCancellableCoroutine { continuation ->
val cardId = if (card.backupStatus?.isActive == true) null else card.cardId
val task = SignHashesTask(hashes, publicKey)
@ -37,13 +37,17 @@ class TangemSigner(
signerCallback(
TangemSignerResponse(
result.data.totalSignedHashes,
result.data.remainingSignatures
)
result.data.remainingSignatures,
),
)
continuation.resume(CompletionResult.Success(result.data.signatures))
if (continuation.isActive) {
continuation.resume(CompletionResult.Success(result.data.signatures))
}
}
is CompletionResult.Failure ->
continuation.resume(CompletionResult.Failure(result.error))
if (continuation.isActive) {
continuation.resume(CompletionResult.Failure(result.error))
}
}
}
}
@ -51,11 +55,11 @@ class TangemSigner(
override suspend fun sign(
hash: ByteArray,
publicKey: Wallet.PublicKey
publicKey: Wallet.PublicKey,
): CompletionResult<ByteArray> {
val result = sign(
hashes = listOf(hash),
publicKey = publicKey
publicKey = publicKey,
)
return when (result) {

View file

@ -6,16 +6,18 @@ 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
sealed interface TotalFiatBalance {
val amount: BigDecimal?
object Loading : TotalFiatBalance()
object Loading : TotalFiatBalance {
override val amount: BigDecimal? = null
}
data class Error(
override val amount: BigDecimal,
) : TotalFiatBalance()
override val amount: BigDecimal?,
) : TotalFiatBalance
data class Loaded(
override val amount: BigDecimal,
) : TotalFiatBalance()
) : TotalFiatBalance
}

View file

@ -16,6 +16,7 @@ import java.math.BigDecimal
* 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
* @param isCardSingleToken shows that [Currency] is a card token
* @param isCustom shows that currency is a custom
* */
data class WalletDataModel(
val currency: Currency,
@ -24,6 +25,7 @@ data class WalletDataModel(
val existentialDeposit: BigDecimal?,
val fiatRate: BigDecimal?,
val isCardSingleToken: Boolean,
val isCustom: Boolean,
val historyTransactions: List<TransactionData>?,
) {
@ -59,9 +61,7 @@ data class WalletDataModel(
data class NoAccount(
val amountToCreateAccount: BigDecimal?,
) : Status() {
override val isErrorStatus: Boolean = true
}
) : Status()
data class Unreachable(
override val errorMessage: String?,

View file

@ -2,12 +2,14 @@ package com.tangem.tap.domain.model.builders
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.domain.common.util.UserWalletId
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.tap.common.extensions.getBlockchainTxHistory
import com.tangem.tap.common.extensions.getTokenTxHistory
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
@ -26,23 +28,23 @@ interface WalletStoreBuilder {
companion object {
operator fun invoke(
userWalletId: UserWalletId,
userWallet: UserWallet,
blockchainNetwork: BlockchainNetwork,
): BlockchainNetworkWalletStoreBuilder {
return BlockchainNetworkWalletStoreBuilderImpl(userWalletId, blockchainNetwork)
return BlockchainNetworkWalletStoreBuilderImpl(userWallet, blockchainNetwork)
}
operator fun invoke(
userWalletId: UserWalletId,
userWallet: UserWallet,
walletManager: WalletManager,
): WalletMangerWalletStoreBuilder {
return WalletMangerWalletStoreBuilderImpl(userWalletId, walletManager)
return WalletMangerWalletStoreBuilderImpl(userWallet, walletManager)
}
}
}
private class BlockchainNetworkWalletStoreBuilderImpl(
private val userWalletId: UserWalletId,
private val userWallet: UserWallet,
private val blockchainNetwork: BlockchainNetwork,
) : WalletStoreBuilder.BlockchainNetworkWalletStoreBuilder {
private var walletManager: WalletManager? = null
@ -52,11 +54,12 @@ private class BlockchainNetworkWalletStoreBuilderImpl(
}
override fun build(): WalletStoreModel {
val blockchainWalletData = blockchainNetwork.getBlockchainWalletData(walletManager)
val tokensWalletsData = blockchainNetwork.getTokensWalletsData(walletManager)
val cardDerivationStyle = userWallet.scanResponse.card.derivationStyle
val blockchainWalletData = blockchainNetwork.getBlockchainWalletData(walletManager, cardDerivationStyle)
val tokensWalletsData = blockchainNetwork.getTokensWalletsData(walletManager, cardDerivationStyle)
return WalletStoreModel(
userWalletId = userWalletId,
userWalletId = userWallet.walletId,
blockchain = blockchainNetwork.blockchain,
derivationPath = blockchainNetwork.derivationPath?.let { DerivationPath(it) },
walletsData = listOf(blockchainWalletData) + tokensWalletsData,
@ -68,7 +71,7 @@ private class BlockchainNetworkWalletStoreBuilderImpl(
}
private class WalletMangerWalletStoreBuilderImpl(
private val userWalletId: UserWalletId,
private val userWallet: UserWallet,
private val walletManager: WalletManager,
) : WalletStoreBuilder.WalletMangerWalletStoreBuilder {
@ -78,7 +81,7 @@ private class WalletMangerWalletStoreBuilderImpl(
val tokenWalletsData = wallet.getTokens().firstOrNull()?.toTokenWalletData(walletManager)
return WalletStoreModel(
userWalletId = userWalletId,
userWalletId = userWallet.walletId,
blockchain = wallet.blockchain,
derivationPath = wallet.publicKey.derivationPath,
walletsData = listOf(blockchainWalletData) + listOfNotNull(tokenWalletsData),
@ -89,35 +92,45 @@ private class WalletMangerWalletStoreBuilderImpl(
}
}
private fun BlockchainNetwork.getBlockchainWalletData(walletManager: WalletManager?): WalletDataModel {
private fun BlockchainNetwork.getBlockchainWalletData(
walletManager: WalletManager?,
cardDerivationStyle: DerivationStyle?,
): WalletDataModel {
val currency = Currency.Blockchain(
blockchain = blockchain,
derivationPath = derivationPath,
)
return WalletDataModel(
currency = Currency.Blockchain(
blockchain = blockchain,
derivationPath = derivationPath,
),
currency = currency,
status = WalletDataModel.Loading,
walletAddresses = walletManager?.wallet?.createAddressesData().orEmpty(),
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
isCardSingleToken = false,
isCustom = currency.isCustomCurrency(cardDerivationStyle),
historyTransactions = walletManager?.getBlockchainTxHistory(),
)
}
private fun BlockchainNetwork.getTokensWalletsData(walletManager: WalletManager?): List<WalletDataModel> {
private fun BlockchainNetwork.getTokensWalletsData(
walletManager: WalletManager?,
cardDerivationStyle: DerivationStyle?,
): List<WalletDataModel> {
return this.tokens
.map { token ->
val currency = Currency.Token(
token = token,
blockchain = blockchain,
derivationPath = derivationPath,
)
WalletDataModel(
currency = Currency.Token(
token = token,
blockchain = blockchain,
derivationPath = derivationPath,
),
currency = currency,
status = WalletDataModel.Loading,
walletAddresses = walletManager?.wallet?.createAddressesData().orEmpty(),
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
isCardSingleToken = walletManager?.cardTokens?.contains(token) ?: false,
isCustom = currency.isCustomCurrency(cardDerivationStyle),
historyTransactions = walletManager?.getTokenTxHistory(token),
)
}
@ -135,6 +148,7 @@ private fun Blockchain.toBlockchainWalletData(walletManager: WalletManager): Wal
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
isCardSingleToken = false,
isCustom = false,
historyTransactions = walletManager.getBlockchainTxHistory(),
)
}
@ -152,6 +166,7 @@ private fun Token.toTokenWalletData(walletManager: WalletManager): WalletDataMod
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
isCardSingleToken = walletManager.cardTokens.contains(this),
isCustom = false,
historyTransactions = walletManager.getTokenTxHistory(this),
)
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.domain.scanCard
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
@ -46,7 +45,6 @@ import kotlinx.coroutines.launch
object ScanCardProcessor {
suspend fun scan(
analyticsEvent: AnalyticsEvent? = null,
additionalBlockchainsToDerive: Collection<Blockchain>? = null,
cardId: String? = null,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {},
onScanStateChange: suspend (scanInProgress: Boolean) -> Unit = {},
@ -63,7 +61,6 @@ object ScanCardProcessor {
val result = tangemSdkManager.scanProduct(
userTokensRepository = userTokensRepository,
cardId = cardId,
additionalBlockchainsToDerive = additionalBlockchainsToDerive,
)
store.dispatchOnMain(GlobalAction.ScanFailsCounter.ChooseBehavior(result))

View file

@ -49,11 +49,9 @@ class ScanProductTask(
val card: Card? = null,
private val userTokensRepository: UserTokensRepository?,
private val additionalBlockchainsToDerive: Collection<Blockchain>? = null,
override val allowsRequestAccessCodeFromRepository: Boolean = false,
) : CardSessionRunnable<ScanResponse> {
override val allowsRequestAccessCodeFromRepository: Boolean
get() = !additionalBlockchainsToDerive.isNullOrEmpty()
override fun run(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
@ -186,6 +184,11 @@ private class ScanWalletProcessor(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
if (card.wallets.isNotEmpty() && card.backupStatus?.isActive == true) {
startLinkingForBackupIfNeeded(card, session, callback)
return
}
if (card.wallets.isEmpty() || !card.isFirmwareMultiwalletAllowed) {
startLinkingForBackupIfNeeded(card, session, callback)
return

View file

@ -27,7 +27,9 @@ internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator {
when (walletsData.findStatus()) {
TotalFiatBalanceStatus.Loading -> TotalFiatBalance.Loading
TotalFiatBalanceStatus.Error -> TotalFiatBalance.Error(calculateAmount())
TotalFiatBalanceStatus.Loaded -> TotalFiatBalance.Loaded(calculateAmount())
TotalFiatBalanceStatus.Loaded -> TotalFiatBalance.Loaded(
amount = calculateAmount() ?: BigDecimal.ZERO,
)
}
}
}
@ -47,7 +49,8 @@ internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator {
is WalletDataModel.VerifiedOnline,
is WalletDataModel.SameCurrencyTransactionInProgress,
is WalletDataModel.TransactionInProgress,
is WalletDataModel.NoAccount -> if (walletData.fiatRate == null) {
is WalletDataModel.NoAccount,
-> if (walletData.isCustom || walletData.fiatRate == null) {
TotalFiatBalanceStatus.Error
} else {
TotalFiatBalanceStatus.Loaded
@ -60,14 +63,17 @@ internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator {
}
}
private fun Sequence<WalletDataModel>.calculateTotalFiatAmount(): BigDecimal {
private fun Sequence<WalletDataModel>.calculateTotalFiatAmount(): BigDecimal? {
return this
.filterNot { it.isCustom }
.map { walletData ->
walletData.fiatRate
?.takeUnless { walletData.status.isErrorStatus }
?.let { walletData.status.amount.toFiatValue(it) }
?: BigDecimal.ZERO
}
.reduce(BigDecimal::plus)
.reduce { acc, value ->
value?.let { acc?.plus(it) }
}
}
private fun getCurrentStatus(
@ -88,8 +94,6 @@ internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator {
}
private enum class TotalFiatBalanceStatus {
Loading,
Error,
Loaded,
Loading, Error, Loaded,
}
}

View file

@ -110,21 +110,26 @@ internal class BiometricUserWalletsListManager(
}
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> {
if (userWalletIds.isEmpty()) {
val idsToRemove = state.value.userWallets
.takeIf { it.isNotEmpty() }
?.filter { it.walletId in userWalletIds }
?.map { it.walletId }
if (idsToRemove.isNullOrEmpty()) {
return CompletionResult.Success(Unit)
}
changeSelectedUserWalletIdIfNeeded(userWalletIds)
changeSelectedUserWalletIdIfNeeded(idsToRemove)
return sensitiveInformationRepository.delete(userWalletIds)
.flatMap { publicInformationRepository.delete(userWalletIds) }
.flatMap { keysRepository.delete(userWalletIds) }
return sensitiveInformationRepository.delete(idsToRemove)
.flatMap { publicInformationRepository.delete(idsToRemove) }
.flatMap { keysRepository.delete(idsToRemove) }
.map {
state.update { prevState ->
val newUserWallets = prevState.userWallets.filter { it.walletId !in userWalletIds }
val newUserWallets = prevState.userWallets.filter { it.walletId !in idsToRemove }
prevState.copy(
encryptionKeys = prevState.encryptionKeys.filter { it.walletId !in userWalletIds },
encryptionKeys = prevState.encryptionKeys.filter { it.walletId !in idsToRemove },
userWallets = newUserWallets,
isLocked = newUserWallets.any { it.isLocked },
)

View file

@ -55,12 +55,16 @@ internal class DefaultWalletCurrenciesManager(
currenciesToAdd: List<Currency>,
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
val card = userWallet.scanResponse.card
val newCurrencies = (getSavedCurrencies(userWallet.walletId) + currenciesToAdd)
.addMissingBlockchains(card)
val currenciesToAddWithMissingBlockchains = currenciesToAdd.addMissingBlockchainsIfNeeded(card)
updateWalletStores(userWallet, newCurrencies.toBlockchainNetworks())
updateWalletStores(
userWallet = userWallet,
blockchainNetworks = currenciesToAddWithMissingBlockchains
.toBlockchainNetworks()
.addSameBlockchainTokens(userWallet.walletId),
)
.map {
saveUserCurrencies(card, newCurrencies)
saveUserCurrencies(card, getSavedCurrencies(userWallet.walletId))
}
.flatMap {
updateWalletStoresAmounts(
@ -114,7 +118,38 @@ internal class DefaultWalletCurrenciesManager(
}
}
private fun List<Currency>.addMissingBlockchains(card: CardDTO): List<Currency> {
// TODO: Need refactoring
private suspend fun List<BlockchainNetwork>.addSameBlockchainTokens(
userWalletId: UserWalletId,
): List<BlockchainNetwork> {
val networks = arrayListOf<BlockchainNetwork>()
val savedWalletStores = withContext(Dispatchers.Default) {
walletStoresRepository.getSync(userWalletId)
}
this.forEach { network ->
val walletStore = savedWalletStores.firstOrNull {
it.blockchain == network.blockchain && it.derivationPath?.rawPath == network.derivationPath
}
if (walletStore != null) {
val tokens = walletStore.walletsData
.asSequence()
.map { it.currency }
.filterIsInstance<Currency.Token>()
.map { it.token }
.toList()
networks.add(network.copy(tokens = tokens + network.tokens.toSet()))
} else {
networks.add(network)
}
}
return networks
}
private fun List<Currency>.addMissingBlockchainsIfNeeded(card: CardDTO): List<Currency> {
if (this.isEmpty()) return this
val currencies = this.asSequence()
@ -165,7 +200,6 @@ internal class DefaultWalletCurrenciesManager(
userWallet: UserWallet,
blockchainNetworks: List<BlockchainNetwork>,
): CompletionResult<Unit> {
val userWalletId = userWallet.walletId
return blockchainNetworks
.map { blockchainNetwork ->
walletManagersRepository.findOrMakeMultiCurrencyWalletManager(
@ -174,8 +208,8 @@ internal class DefaultWalletCurrenciesManager(
)
.flatMap { walletManager ->
walletStoresRepository.storeOrUpdate(
userWalletId = userWalletId,
walletStore = WalletStoreBuilder(userWalletId, blockchainNetwork)
userWalletId = userWallet.walletId,
walletStore = WalletStoreBuilder(userWallet, blockchainNetwork)
.walletManager(walletManager)
.build(),
)

View file

@ -137,7 +137,7 @@ internal class DefaultWalletStoresManager(
{ walletManager ->
walletStoresRepository.storeOrUpdate(
userWalletId = userWalletId,
walletStore = WalletStoreBuilder(userWalletId, blockchainNetwork)
walletStore = WalletStoreBuilder(userWallet, blockchainNetwork)
.walletManager(walletManager)
.build(),
)
@ -171,7 +171,7 @@ internal class DefaultWalletStoresManager(
val userWalletId = userWallet.walletId
walletStoresRepository.storeOrUpdate(
userWalletId = userWalletId,
walletStore = WalletStoreBuilder(userWalletId, walletManager)
walletStore = WalletStoreBuilder(userWallet, walletManager)
.build(),
)
}

View file

@ -442,7 +442,7 @@ internal class DefaultWalletAmountsRepository(
.toMutableList()
.apply {
replaceByOrAdd(walletManager) {
it.wallet.blockchain == it.wallet.blockchain
it.wallet.blockchain == walletManager.wallet.blockchain
}
}

View file

@ -7,8 +7,8 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.CompletionResult
import com.tangem.common.catching
import com.tangem.common.doOnSuccess
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.common.map
import com.tangem.common.mapFailure
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
@ -22,7 +22,7 @@ import com.tangem.tap.domain.walletStores.WalletStoresError
import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
import com.tangem.tap.domain.walletStores.storage.WalletManagerStorage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
import timber.log.Timber
@ -93,10 +93,7 @@ internal class DefaultWalletManagersRepository(
scanResponse = scanResponse,
blockchainNetwork = blockchainNetwork,
)
.map { updatedWalletManager ->
store(userWallet.walletId, updatedWalletManager)
updatedWalletManager
}
.doOnSuccess { store(userWallet.walletId, it) }
}
else -> {
val error = WalletStoresError.WalletManagerNotCreated(blockchain)
@ -158,8 +155,9 @@ internal class DefaultWalletManagersRepository(
return catching {
val tokens = blockchainNetwork?.tokens ?: listOfNotNull(scanResponse.cardTypesResolver.getPrimaryToken())
if (tokens != cardTokens) {
cardTokens.clear()
if (tokens != walletManager.cardTokens) {
walletManager.cardTokens.clear()
walletManager.wallet.removeAllTokens()
if (tokens.isNotEmpty()) {
walletManager.cardTokens.addAll(tokens)
}
@ -181,13 +179,16 @@ internal class DefaultWalletManagersRepository(
userWalletId: UserWalletId,
blockchain: Blockchain?,
): WalletManager? {
return walletManagersStorage.getAll().first()[userWalletId]?.let { userWalletManagers ->
if (blockchain == null) {
userWalletManagers.firstOrNull()
} else {
userWalletManagers.find { it.wallet.blockchain == blockchain }
return walletManagersStorage.getAll()
.firstOrNull()
?.get(userWalletId)
?.let { userWalletManagers ->
if (blockchain == null) {
userWalletManagers.firstOrNull()
} else {
userWalletManagers.firstOrNull { it.wallet.blockchain == blockchain }
}
}
}
}
private fun getDerivationParams(derivationPath: String?, card: CardDTO): DerivationParams? {

View file

@ -68,7 +68,10 @@ class DetailsMiddleware {
}
DetailsAction.ScanCard -> {
scope.launch {
tangemSdkManager.scanProduct(userTokensRepository)
tangemSdkManager.scanProduct(
userTokensRepository = userTokensRepository,
allowsRequestAccessCodeFromRepository = true,
)
.doOnSuccess { scanResponse ->
val currentUserWalletId = state.scanResponse
?.let { UserWalletIdBuilder.scanResponse(it).build() }

View file

@ -2,9 +2,7 @@ package com.tangem.tap.features.details.redux
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.extensions.isWalletDataSupported
import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.preferencesStorage
import com.tangem.tap.store
@ -109,11 +107,9 @@ private fun prepareSecurityOptions(card: CardDTO, cardTypesResolver: CardTypesRe
}
private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: CardTypesResolver): Boolean {
val notAllowedByAnyWallet = card.wallets.any { it.settings.isPermanent }
val notAllowedByCard = notAllowedByAnyWallet ||
card.isWalletDataSupported && !cardTypesResolver.isTangemNote() && !card.settings.isBackupAllowed ||
card.isSaltPay
return !notAllowedByCard
val hasPermanentWallet = card.wallets.any { it.settings.isPermanent }
val isNotAllowed = hasPermanentWallet || cardTypesResolver.isSaltPay() || cardTypesResolver.isStart2Coin()
return !isNotAllowed
}
private fun handleEraseWallet(

View file

@ -74,7 +74,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
fun updateScanResponse(response: ScanResponse) {
when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse = response
CreateTwinWalletMode.RecreateWallet -> store.dispatch(GlobalAction.SaveScanResponse(response))
CreateTwinWalletMode.RecreateWallet -> store.dispatchOnMain(GlobalAction.SaveScanResponse(response))
}
}
@ -322,8 +322,9 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
OnboardingHelper.onInterrupted()
store.dispatch(TwinCardsAction.CardsManager.Release)
action.shouldResetTwinCardsWidget(shouldReturnCardBack) {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatchOnMain(NavigationAction.PopBackTo(getPopBackScreen()))
}
}
store.dispatchDialogShow(OnboardingDialog.InterruptOnboarding(onOkCallback))
@ -331,4 +332,16 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
}
else -> Unit
}
}
private fun getPopBackScreen(): AppScreen {
return if (userWalletsListManager.hasSavedUserWallets) {
if (userWalletsListManager.isLockedSync) {
AppScreen.Welcome
} else {
AppScreen.Wallet
}
} else {
AppScreen.Home
}
}

View file

@ -15,10 +15,10 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.datasource.utils.AndroidAssetReader
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.datasource.utils.AndroidAssetReader
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.getDrawableCompat

View file

@ -207,9 +207,8 @@ class TokensMiddleware {
}
scope.launch {
val card = scanResponse.card
val result = tangemSdkManager.derivePublicKeys(
cardId = card.cardId,
cardId = null,
derivations = derivations,
)
when (result) {
@ -232,7 +231,6 @@ class TokensMiddleware {
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens"))
}

View file

@ -6,6 +6,6 @@ import java.math.BigDecimal
data class TotalBalance(
val state: ProgressState,
val fiatAmount: BigDecimal,
val fiatAmount: BigDecimal?,
val fiatCurrency: FiatCurrency,
)

View file

@ -5,8 +5,8 @@ import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.common.flatMap
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken
import com.tangem.tap.common.extensions.addContext
@ -21,7 +21,6 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.demo.isDemoCard
@ -33,12 +32,12 @@ import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.features.wallet.redux.reducers.toWallet
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletCurrenciesManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
class MultiWalletMiddleware {
@ -195,24 +194,26 @@ class MultiWalletMiddleware {
state: WalletState?,
) = scope.launch(Dispatchers.Default) {
dispatchOnMain(WalletAction.MultiWallet.ScheduleCheckForMissingDerivation)
ScanCardProcessor.scan(
analyticsEvent = null,
tangemSdkManager.scanProduct(
cardId = selectedUserWallet.cardId,
userTokensRepository = userTokensRepository,
additionalBlockchainsToDerive = state?.missingDerivations?.map { it.blockchain },
) { scanResponse ->
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { userWallet ->
userWallet.copy(
scanResponse = scanResponse,
)
},
)
.doOnSuccess { updatedUserWallet ->
store.dispatchOnMain(WalletAction.MultiWallet.AddMissingDerivations(emptyList()))
store.state.globalState.tapWalletManager.loadData(updatedUserWallet, refresh = true)
}
}
allowsRequestAccessCodeFromRepository = true,
)
.flatMap { scanResponse ->
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { userWallet ->
userWallet.copy(
scanResponse = scanResponse,
)
},
)
}
.doOnSuccess { updatedUserWallet ->
store.dispatchOnMain(WalletAction.MultiWallet.AddMissingDerivations(emptyList()))
store.state.globalState.tapWalletManager.loadData(updatedUserWallet, refresh = true)
}
}
private fun addDummyBalances(walletManagers: List<WalletManager>) {
@ -251,10 +252,16 @@ class MultiWalletMiddleware {
scope.launch { userTokensRepository.saveUserTokens(scanResponse.card, currencies) }
}
} ?: wmFactory.makeWalletManagerForApp(scanResponse, blockchainNetwork)?.also {
store.dispatch(WalletAction.MultiWallet.AddBlockchain(blockchainNetwork.updateTokens(tokens), it, save))
store.dispatchOnMain(
WalletAction.MultiWallet.AddBlockchain(
blockchain = blockchainNetwork.updateTokens(tokens),
walletManager = it,
save = save,
),
)
}
store.dispatch(
store.dispatchOnMain(
WalletAction.LoadFiatRate(
coinsList = tokens.map { token ->
Currency.Token(
@ -268,30 +275,25 @@ class MultiWalletMiddleware {
if (tokens.isNotEmpty()) walletManager?.addTokens(tokens)
scope.launch {
val result = walletManager?.safeUpdate()
withMainContext {
when (result) {
is com.tangem.common.services.Result.Success -> {
val wallet = result.data
wallet.getTokens()
.filter { tokens.contains(it) }
.mapNotNull { token ->
wallet.getTokenAmount(token)?.let { Pair(token, it) }
}
.forEach {
withContext(Dispatchers.Main) {
store.dispatch(
WalletAction.MultiWallet.TokenLoaded(
it.second,
it.first,
blockchainNetwork,
),
)
}
}
}
else -> {}
when (val result = walletManager?.safeUpdate()) {
is com.tangem.common.services.Result.Success -> {
val wallet = result.data
wallet.getTokens()
.filter { tokens.contains(it) }
.mapNotNull { token ->
wallet.getTokenAmount(token)?.let { amount -> Pair(token, amount) }
}
.forEach { (token, tokenAmount) ->
store.dispatchOnMain(
WalletAction.MultiWallet.TokenLoaded(
amount = tokenAmount,
token = token,
blockchain = blockchainNetwork,
),
)
}
}
else -> Unit
}
}
}

View file

@ -5,6 +5,7 @@ import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchToastNotification
import com.tangem.tap.common.extensions.getBlockchainTxHistory
import com.tangem.tap.common.extensions.getTokenTxHistory
@ -113,14 +114,17 @@ class MultiWalletReducer {
val currency = Currency.fromBlockchainNetwork(action.blockchain, action.token)
val walletManager = state.getWalletManager(currency)
if (walletManager == null) {
if (userWalletsListManager.hasSavedUserWallets) {
store.dispatch(NavigationAction.PopBackTo(screen = AppScreen.Welcome))
val screen = if (userWalletsListManager.hasSavedUserWallets) {
AppScreen.Welcome
} else {
store.dispatch(NavigationAction.PopBackTo(screen = AppScreen.Home))
AppScreen.Home
}
store.dispatchOnMain(NavigationAction.PopBackTo(screen))
FirebaseCrashlytics.getInstance().recordException(
IllegalStateException("MultiWallet.TokenLoaded: walletManager is null"),
)
store.dispatchToastNotification(R.string.internal_error_wallet_manager_not_found)
return state
}

View file

@ -42,6 +42,7 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.models.WalletWarning
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.ErrorType
@ -333,8 +334,11 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
}
private fun showPendingTransactionsIfPresent(pendingTransactions: List<PendingTransaction>) {
pendingTransactionAdapter.submitList(pendingTransactions)
binding.rvPendingTransaction.show(pendingTransactions.isNotEmpty())
val knownTransactions = pendingTransactions.filterNot {
it.type == PendingTransactionType.Unknown
}
pendingTransactionAdapter.submitList(knownTransactions)
binding.rvPendingTransaction.show(knownTransactions.isNotEmpty())
}
private fun setupAddressCard(

View file

@ -0,0 +1,340 @@
package com.tangem.tap.features.wallet.ui.view
import android.content.Context
import android.util.AttributeSet
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Divider
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SelectorButton
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.components.SpacerW16
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.formatWithSpaces
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
import com.tangem.wallet.R
import com.valentinilk.shimmer.shimmer
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import java.util.*
internal class TotalBalanceCard @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : AbstractComposeView(context, attrs, defStyleAttr) {
private var state by mutableStateOf<TotalBalanceCardState>(TotalBalanceCardState.Empty)
var status: TotalBalance? = null
set(value) {
if (field == value) return
field = value
updateState(value, onChangeFiatCurrencyClick)
}
var onChangeFiatCurrencyClick: () -> Unit = { /* no-op */ }
set(value) {
if (field == value) return
field = value
updateState(status, value)
}
@Composable
override fun Content() {
TangemTheme {
TotalBalanceCardContent(state = state)
}
}
override fun getAccessibilityClassName(): CharSequence {
return javaClass.name
}
private fun updateState(status: TotalBalance?, onChangeCurrencyClick: () -> Unit) {
state = when (status?.state) {
null -> TotalBalanceCardState.Empty
ProgressState.Loading -> TotalBalanceCardState.Loading(
fiatCurrency = status.fiatCurrency,
onChangeFiatCurrencyClick = onChangeCurrencyClick,
)
ProgressState.Error -> TotalBalanceCardState.Failure(
amount = status.fiatAmount,
fiatCurrency = status.fiatCurrency,
onChangeFiatCurrencyClick = onChangeCurrencyClick,
)
ProgressState.Refreshing,
ProgressState.Done,
-> TotalBalanceCardState.Success(
amount = status.fiatAmount ?: BigDecimal.ZERO,
fiatCurrency = status.fiatCurrency,
onChangeFiatCurrencyClick = onChangeCurrencyClick,
)
}
}
}
@Composable
private fun TotalBalanceCardContent(
state: TotalBalanceCardState,
modifier: Modifier = Modifier,
) {
TotalBalanceCardScaffold(
modifier = modifier,
title = {
Text(
text = stringResource(id = R.string.main_page_balance),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
},
amount = {
when (state) {
is TotalBalanceCardState.Empty,
is TotalBalanceCardState.Loading,
-> LoadingAmount()
is TotalBalanceCardState.Failure,
is TotalBalanceCardState.Success,
-> LoadedAmount(
amount = buildAmountString(
amount = state.amount,
fiatCurrencySymbol = state.fiatCurrency.symbol,
),
)
}
},
currencySelector = {
if (state !is TotalBalanceCardState.Empty) {
SelectorButton(
text = state.fiatCurrency.code,
onClick = state.onChangeFiatCurrencyClick,
)
}
},
failureText = {
AnimatedVisibility(visible = state is TotalBalanceCardState.Failure) {
Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.main_processing_full_amount),
style = TangemTheme.typography.caption,
color = TangemTheme.colors.text.attention,
)
}
},
)
}
@Composable
private fun TotalBalanceCardScaffold(
title: @Composable () -> Unit,
amount: @Composable () -> Unit,
currencySelector: @Composable () -> Unit,
failureText: @Composable () -> Unit,
modifier: Modifier = Modifier,
amountWeight: Float = 0.8f,
) {
Surface(
modifier = modifier,
shape = TangemTheme.shapes.roundedCornersMedium,
color = TangemTheme.colors.background.plain,
elevation = TangemTheme.dimens.elevation1,
) {
Column(
modifier = Modifier.fillMaxWidth(),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top,
) {
SpacerW16()
Column(
modifier = Modifier.weight(amountWeight),
) {
SpacerH12()
title()
SpacerH4()
amount()
}
currencySelector()
SpacerW4()
}
SpacerH4()
Box(
modifier = Modifier.padding(
horizontal = TangemTheme.dimens.spacing16,
),
) {
failureText()
}
SpacerH12()
}
}
}
@Composable
private fun LoadingAmount(
modifier: Modifier = Modifier,
) {
Box(modifier = modifier.shimmer()) {
Box(
modifier = Modifier
.width(TangemTheme.dimens.size116)
.height(TangemTheme.dimens.size32)
.background(
color = TangemTheme.colors.stroke.primary,
shape = TangemTheme.shapes.roundedCornersSmall2,
),
)
}
}
@Composable
private fun LoadedAmount(
amount: AnnotatedString,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
Text(
text = amount,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
@Composable
private fun buildAmountString(
amount: BigDecimal?,
fiatCurrencySymbol: String,
): AnnotatedString {
if (amount == null) return AnnotatedString(text = UNKNOWN_AMOUNT_SIGN)
val format = DecimalFormat.getInstance(Locale.getDefault()) as DecimalFormat
val scaledAmount = amount
.setScale(2, RoundingMode.HALF_UP)
.formatWithSpaces()
val integer = scaledAmount.substringBefore('.')
val reminder = scaledAmount.substringAfter('.')
return buildAnnotatedString {
append(integer)
append(format.decimalFormatSymbols.decimalSeparator)
append(
AnnotatedString(
text = "$reminder $fiatCurrencySymbol",
spanStyle = TangemTheme.typography.h3.toSpanStyle(),
),
)
}
}
private sealed interface TotalBalanceCardState {
val amount: BigDecimal?
val onChangeFiatCurrencyClick: () -> Unit
val fiatCurrency: FiatCurrency
object Empty : TotalBalanceCardState {
override val amount: BigDecimal? = null
override val fiatCurrency: FiatCurrency = FiatCurrency.Default
override val onChangeFiatCurrencyClick: () -> Unit = { /* no-op */ }
}
data class Loading(
override val fiatCurrency: FiatCurrency,
override val onChangeFiatCurrencyClick: () -> Unit,
) : TotalBalanceCardState {
override val amount: BigDecimal = BigDecimal.ZERO
}
data class Failure(
override val amount: BigDecimal?,
override val fiatCurrency: FiatCurrency,
override val onChangeFiatCurrencyClick: () -> Unit,
) : TotalBalanceCardState
data class Success(
override val amount: BigDecimal,
override val fiatCurrency: FiatCurrency,
override val onChangeFiatCurrencyClick: () -> Unit,
) : TotalBalanceCardState
}
// region Preview
@Composable
private fun TotalBalanceCardContentSample(
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.padding(all = TangemTheme.dimens.spacing16),
) {
TotalBalanceCardContent(state = TotalBalanceCardState.Empty)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
TotalBalanceCardContent(
state = TotalBalanceCardState.Loading(FiatCurrency("USD", "USD", "$")) {},
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
TotalBalanceCardContent(
state = TotalBalanceCardState.Failure(
amount = BigDecimal("9917.72"),
onChangeFiatCurrencyClick = {},
fiatCurrency = FiatCurrency("USD", "USD", "$"),
),
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
TotalBalanceCardContent(
state = TotalBalanceCardState.Success(
amount = BigDecimal("9917.72"),
onChangeFiatCurrencyClick = {},
fiatCurrency = FiatCurrency("USD", "USD", "$"),
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun TotalBalanceCardContentPreview_Light() {
TangemTheme {
TotalBalanceCardContentSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun TotalBalanceCardContentPreview_Dark() {
TangemTheme(isDark = true) {
TotalBalanceCardContentSample()
}
}
// endregion Preview

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.wallet.ui.wallet
import android.widget.Button
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.badoo.mvicore.DiffStrategy
@ -11,8 +12,6 @@ import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.common.analytics.events.Portfolio
import com.tangem.tap.common.extensions.animateVisibility
import com.tangem.tap.common.extensions.formatAmountAsSpannedString
import com.tangem.tap.common.extensions.getQuantityString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
@ -95,7 +94,7 @@ class MultiWalletView : WalletView() {
lAddress.root.hide()
rowButtons.hide()
lSingleWalletBalance.root.hide()
lCardTotalBalance.root.show()
lCardTotalBalance.show()
rvMultiwallet.show()
btnAddToken.show()
}
@ -183,37 +182,12 @@ class MultiWalletView : WalletView() {
progressState: ProgressState,
walletsCount: Int,
) = with(binding.lCardTotalBalance) {
if (walletsCount == 0) {
root.isVisible = false
} else {
if (totalBalance == null) {
if (progressState != ProgressState.Loading) {
root.isVisible = false
}
} else {
root.isVisible = true
// Skip changes when on refreshing state
if (totalBalance.state == ProgressState.Refreshing || progressState == ProgressState.Refreshing) {
return@with
}
isGone = progressState == ProgressState.Done && walletsCount == 0
if (totalBalance.state == ProgressState.Loading) {
veilBalance.veil()
} else {
veilBalance.unVeil()
}
tvProcessing.animateVisibility(show = totalBalance.state == ProgressState.Error)
tvBalance.text = totalBalance.fiatAmount.formatAmountAsSpannedString(
currencySymbol = totalBalance.fiatCurrency.symbol,
)
tvCurrencyName.text = totalBalance.fiatCurrency.code
tvCurrencyName.setOnClickListener {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
}
onChangeFiatCurrencyClick = {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
status = totalBalance
}
private fun handleErrorStates(

View file

@ -41,7 +41,7 @@ class SingleWalletView : WalletView() {
btnAddToken.hide()
rvPendingTransaction.hide()
pbLoadingUserTokens.hide()
lCardTotalBalance.root.hide()
lCardTotalBalance.hide()
lSingleWalletBalance.root.hide()
lWalletRescanWarning.root.hide()
lWalletBackupWarning.root.hide()

View file

@ -3,23 +3,24 @@ 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.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
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
import java.math.BigDecimal
internal fun List<UserWalletModel>.toUiModels(
appCurrency: FiatCurrency,
): Sequence<UserWalletItem> {
return this.asSequence().map { userWalletModel ->
with(userWalletModel) {
val formatAmount = { amount: BigDecimal ->
amount.toFormattedFiatValue(appCurrency.symbol)
}
val balance = when (fiatBalance) {
is TotalFiatBalance.Error -> UserWalletItem.Balance.Error(formatAmount(fiatBalance.amount))
is TotalFiatBalance.Loaded -> UserWalletItem.Balance.Loaded(formatAmount(fiatBalance.amount))
is TotalFiatBalance.Error -> UserWalletItem.Balance.Error(
amount = fiatBalance.amount?.toFormattedFiatValue(appCurrency.symbol) ?: UNKNOWN_AMOUNT_SIGN,
)
is TotalFiatBalance.Loaded -> UserWalletItem.Balance.Loaded(
amount = fiatBalance.amount.toFormattedFiatValue(appCurrency.symbol),
)
is TotalFiatBalance.Loading -> UserWalletItem.Balance.Loading
}
when (type) {

View file

@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
@ -31,6 +30,8 @@ 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
@ -38,6 +39,7 @@ 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.common.util.UserWalletId
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.common.extensions.cardImageData
import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem
@ -137,23 +139,21 @@ private fun WalletCardImage(
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun RowScope.WalletInfo(
wallet: UserWalletItem,
isSelected: Boolean,
) {
private fun RowScope.WalletInfo(wallet: UserWalletItem, isSelected: Boolean) {
Column(
modifier = Modifier.weight(weight = .6f),
modifier = Modifier.weight(weight = 2f),
verticalArrangement = Arrangement.SpaceAround,
horizontalAlignment = Alignment.Start,
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = wallet.name,
color = if (isSelected) TangemTheme.colors.text.accent else TangemTheme.colors.text.primary1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTypography.subtitle1,
)
SpacerH2()
Text(
modifier = Modifier.fillMaxWidth(),
text = when (wallet) {
is MultiCurrencyUserWalletItem -> pluralStringResource(
id = R.plurals.card_label_card_count,
@ -162,20 +162,17 @@ private fun RowScope.WalletInfo(
)
is SingleCurrencyUserWalletItem -> wallet.tokenName
},
style = TangemTheme.typography.caption,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
style = TangemTheme.typography.caption,
)
}
}
@Composable
private fun RowScope.TokensInfo(
isLocked: Boolean,
balance: UserWalletItem.Balance,
tokensCount: Int?,
) {
private fun RowScope.TokensInfo(isLocked: Boolean, balance: UserWalletItem.Balance, tokensCount: Int?) {
Column(
modifier = Modifier.weight(weight = .4f),
modifier = Modifier.weight(weight = 3f),
verticalArrangement = Arrangement.SpaceAround,
horizontalAlignment = Alignment.End,
) {
@ -254,9 +251,11 @@ private fun LoadedTokensInfo(
) {
Text(
text = balanceAmount,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.End,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography.subtitle1,
)
if (showWarning) {
Icon(
@ -371,4 +370,26 @@ private fun CheckedWalletMark(
contentDescription = null,
)
}
}
@Preview(widthDp = 360, heightDp = 72, showBackground = true)
@Composable
private fun PreviewWalletItem() {
TangemTheme {
WalletItem(
wallet = MultiCurrencyUserWalletItem(
id = UserWalletId(value = null),
name = "Tangem Card",
imageUrl = "",
balance = UserWalletItem.Balance.Loaded("141212121888 BTC"),
isLocked = false,
tokensCount = 2,
cardsInWallet = 1,
),
isSelected = false,
isChecked = false,
onWalletClick = {},
onWalletLongClick = {},
)
}
}

View file

@ -26,7 +26,6 @@ import com.tangem.lib.crypto.models.transactions.SendTxResult
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import java.math.BigDecimal
@ -172,6 +171,10 @@ class TransactionManagerImpl(
val error = result.error as? BlockchainSdkError ?: return SendTxResult.UnknownError()
when (error) {
is BlockchainSdkError.WrappedTangemError -> {
val errorByCode = mapErrorByCode(error)
if (errorByCode != null) {
return errorByCode
}
val tangemSdkError = error.tangemError as? TangemSdkError ?: return SendTxResult.UnknownError()
if (tangemSdkError is TangemSdkError.UserCancelled) return SendTxResult.UserCancelledError
return SendTxResult.TangemSdkError(tangemSdkError.code, tangemSdkError.cause)
@ -184,6 +187,17 @@ class TransactionManagerImpl(
}
}
private fun mapErrorByCode(error: BlockchainSdkError.WrappedTangemError): SendTxResult? {
return when (error.code) {
USER_CANCELLED_ERROR_CODE -> {
return SendTxResult.UserCancelledError
}
else -> {
null
}
}
}
private fun transactionSigner(walletManager: WalletManager): TransactionSigner {
val actualCard = requireNotNull(appStateHolder.getActualCard()) { "no card found" }
return TangemSigner(
@ -191,7 +205,7 @@ class TransactionManagerImpl(
tangemSdk = tangemSdk,
initialMessage = Message(),
) { signResponse ->
store.dispatch(
appStateHolder.mainStore?.dispatch(
GlobalAction.UpdateWalletSignedHashes(
walletSignedHashes = signResponse.totalSignedHashes,
walletPublicKey = walletManager.wallet.publicKey.seedKey,
@ -292,5 +306,6 @@ class TransactionManagerImpl(
companion object {
private const val HEX_PREFIX = "0x"
private const val USER_CANCELLED_ERROR_CODE = 50002
}
}

View file

@ -23,9 +23,12 @@ import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletCurrenciesManager
import kotlinx.coroutines.delay
import org.rekotlin.Action
import java.math.BigDecimal
import com.tangem.tap.features.wallet.models.Currency as WalletCurrency
class UserWalletManagerImpl(
private val appStateHolder: AppStateHolder,
@ -106,13 +109,21 @@ class UserWalletManagerImpl(
val card = requireNotNull(appStateHolder.getActualCard()) { "card not found" }
val blockchain = requireNotNull(Blockchain.fromNetworkId(currency.networkId)) { "blockchain not found" }
val blockchainNetwork = BlockchainNetwork(blockchain, card)
val walletManager = getOrCreateBlockchain(blockchainNetwork, blockchain)
if (currency is NonNativeToken &&
!walletManager.cardTokens.contains(currency.toSdkToken())
) {
val action = addNonNativeTokenToWalletAction(currency, card, blockchain)
val mainStore = requireNotNull(appStateHolder.mainStore) { "mainStore is null" }
mainStore.dispatchOnMain(action)
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
walletCurrenciesManager.addCurrencies(
userWallet = selectedUserWallet,
currenciesToAdd = listOf(currency.toWalletCurrency(blockchainNetwork)),
)
} else {
val walletManager = getOrCreateBlockchain(blockchainNetwork, blockchain)
if (currency is NonNativeToken && !walletManager.cardTokens.contains(currency.toSdkToken())) {
val action = addNonNativeTokenToWalletAction(currency, card, blockchain)
val mainStore = requireNotNull(appStateHolder.mainStore) { "mainStore is null" }
mainStore.dispatchOnMain(action)
delay(DELAY_UPDATE_WALLET)
}
}
}
@ -238,6 +249,11 @@ class UserWalletManagerImpl(
)
}
override fun refreshWallet() {
// workaround, should update wallet after transaction
appStateHolder.mainStore?.dispatchOnMain(WalletAction.LoadData.Refresh)
}
private fun createDerivationParams(derivationStyle: DerivationStyle?): DerivationParams? {
// todo clarify if its need to add Custom
return derivationStyle?.let { DerivationParams.Default(derivationStyle) }
@ -265,4 +281,18 @@ private fun NonNativeToken.toSdkToken(): Token {
contractAddress = this.contractAddress,
decimals = this.decimalCount,
)
}
private fun Currency.toWalletCurrency(network: BlockchainNetwork): WalletCurrency {
return when (this) {
is NativeToken -> WalletCurrency.Blockchain(
blockchain = network.blockchain,
derivationPath = network.derivationPath,
)
is NonNativeToken -> WalletCurrency.Token(
token = this.toSdkToken(),
blockchain = network.blockchain,
derivationPath = network.derivationPath,
)
}
}

View file

@ -53,7 +53,7 @@
android:focusable="true"
android:gravity="center_vertical"
android:padding="16dp"
android:text="@string/swapping_swap"
android:text="@string/swapping_swap_action"
android:textColor="@color/darkGray3"
android:textSize="14sp"
android:textStyle="bold"

View file

@ -47,6 +47,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:clipChildren="false"
android:paddingBottom="92dp">
<ImageView
@ -136,9 +137,8 @@
</LinearLayout>
<include
<com.tangem.tap.features.wallet.ui.view.TotalBalanceCard
android:id="@+id/l_card_total_balance"
layout="@layout/layout_card_total_balance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"

View file

@ -1,84 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card_balance"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="18dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="18dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/main_page_balance"
android:textColor="@color/text_tertiary"
android:textSize="14sp"
app:layout_constraintEnd_toStartOf="@id/tv_currency_name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.skydoves.androidveil.VeilLayout
android:id="@+id/veil_balance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:visibility="visible"
app:layout_constraintBottom_toTopOf="@id/tv_processing"
app:layout_constraintTop_toBottomOf="@id/tv_title"
app:veilLayout_baseColor="@color/lightGray0"
app:veilLayout_highlightColor="@color/lightGray1"
app:veilLayout_layout="@layout/card_total_balance_shimmer"
app:veilLayout_radius="4dp"
app:veilLayout_shimmerEnable="true"
app:veilLayout_veiled="true">
<TextView
android:id="@+id/tv_balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:minWidth="152dp"
android:textColor="@color/text_primary_1"
android:textSize="26sp"
android:textStyle="bold"
android:visibility="visible"
tools:text="22 325.40 $"
tools:visibility="visible" />
</com.skydoves.androidveil.VeilLayout>
<TextView
android:id="@+id/tv_processing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="@string/main_processing_full_amount"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/veil_balance"
tools:visibility="visible" />
<TextView
android:id="@+id/tv_currency_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_tertiary"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_arrow_angle_down"
app:drawableTint="@color/icon_informative"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="USD" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -39,7 +39,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_swap"
style="@style/TapPrimaryIconButton"
android:text="@string/swapping_swap"
android:text="@string/swapping_swap_action"
android:visibility="gone"
app:icon="@drawable/ic_arrows_up_down"
tools:visibility="gone" />

View file

@ -14,7 +14,9 @@ import com.tangem.datasource.config.models.ConfigModel
import com.tangem.datasource.config.models.ConfigValueModel
import com.tangem.datasource.config.models.FeatureModel
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
override var config: Config = Config()

View file

@ -351,8 +351,9 @@
<string name="swapping_success_view_explorer_button_title">View in Explorer</string>
<string name="swapping_success_view_title">In progress</string>
<string name="swapping_swap">Swap</string>
<string name="swapping_swap_action">Swap</string>
<string name="swapping_swap_of_to">Swap of %s to</string>
<string name="swapping_tangem_fee_disclaimer">Additionally, quotes include Tangem fee of %s. This helps us deliver a top-of-the-line product.</string>
<string name="swapping_tangem_fee_disclaimer">Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product.</string>
<string name="swapping_token_list_other_tokens">Other tokens</string>
<string name="swapping_token_list_title">Choose token</string>
<string name="swapping_token_list_your_tokens">Your tokens</string>

View file

@ -351,8 +351,9 @@
<string name="swapping_success_view_explorer_button_title">View in Explorer</string>
<string name="swapping_success_view_title">In progress</string>
<string name="swapping_swap">Swap</string>
<string name="swapping_swap_action">Swap</string>
<string name="swapping_swap_of_to">Swap of %s to</string>
<string name="swapping_tangem_fee_disclaimer">Additionally, quotes include Tangem fee of %s. This helps us deliver a top-of-the-line product.</string>
<string name="swapping_tangem_fee_disclaimer">Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product.</string>
<string name="swapping_token_list_other_tokens">Other tokens</string>
<string name="swapping_token_list_title">Choose token</string>
<string name="swapping_token_list_your_tokens">Your tokens</string>

View file

@ -351,8 +351,9 @@
<string name="swapping_success_view_explorer_button_title">View in Explorer</string>
<string name="swapping_success_view_title">In progress</string>
<string name="swapping_swap">Swap</string>
<string name="swapping_swap_action">Swap</string>
<string name="swapping_swap_of_to">Swap of %s to</string>
<string name="swapping_tangem_fee_disclaimer">Additionally, quotes include Tangem fee of %s. This helps us deliver a top-of-the-line product.</string>
<string name="swapping_tangem_fee_disclaimer">Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product.</string>
<string name="swapping_token_list_other_tokens">Other tokens</string>
<string name="swapping_token_list_title">Choose token</string>
<string name="swapping_token_list_your_tokens">Your tokens</string>

View file

@ -351,8 +351,9 @@
<string name="swapping_success_view_explorer_button_title">Открыть в обозревателе</string>
<string name="swapping_success_view_title">В процессе</string>
<string name="swapping_swap">Обмен</string>
<string name="swapping_swap_action">Обменять</string>
<string name="swapping_swap_of_to">Обмен %s на</string>
<string name="swapping_tangem_fee_disclaimer">Кроме того, котировки включают комиссию Tangem в размере %s. Это помогает нам предоставлять первоклассный продукт.</string>
<string name="swapping_tangem_fee_disclaimer">Котировки включают дополнительную комиссию Tangem в размере %s. Это помогает нам предоставлять первоклассный продукт.</string>
<string name="swapping_token_list_other_tokens">Другие токены</string>
<string name="swapping_token_list_title">Выберите токен</string>
<string name="swapping_token_list_your_tokens">Ваши токены</string>

View file

@ -351,8 +351,9 @@
<string name="swapping_success_view_explorer_button_title">在瀏覽器中查看</string>
<string name="swapping_success_view_title">進行中</string>
<string name="swapping_swap">交換</string>
<string name="swapping_swap_action">Swap</string>
<string name="swapping_swap_of_to">交易 %s 至</string>
<string name="swapping_tangem_fee_disclaimer">Additionally, quotes include Tangem fee of %s. This helps us deliver a top-of-the-line product.</string>
<string name="swapping_tangem_fee_disclaimer">Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product.</string>
<string name="swapping_token_list_other_tokens">其他代幣</string>
<string name="swapping_token_list_title">選擇代幣</string>
<string name="swapping_token_list_your_tokens">您的代幣</string>

View file

@ -351,8 +351,9 @@
<string name="swapping_success_view_explorer_button_title">View in Explorer</string>
<string name="swapping_success_view_title">In progress</string>
<string name="swapping_swap">Swap</string>
<string name="swapping_swap_action">Swap</string>
<string name="swapping_swap_of_to">Swap of %s to</string>
<string name="swapping_tangem_fee_disclaimer">Additionally, quotes include Tangem fee of %s. This helps us deliver a top-of-the-line product.</string>
<string name="swapping_tangem_fee_disclaimer">Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product.</string>
<string name="swapping_token_list_other_tokens">Other tokens</string>
<string name="swapping_token_list_title">Choose token</string>
<string name="swapping_token_list_your_tokens">Your tokens</string>

View file

@ -1,13 +1,12 @@
package com.tangem.core.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -31,11 +30,14 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
// region TextButton
/**
* [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=97%3A103&t=TmfD6UBHPg9uYfev-4)
* */
@ -99,7 +101,9 @@ fun WarningTextButton(
size = TangemButtonSize.Text,
)
}
// endregion TextButton
// region PrimaryButton
@Composable
fun PrimaryButton(
text: String,
@ -164,7 +168,9 @@ fun PrimaryButtonIconLeft(
showProgress = showProgress,
)
}
// endregion PrimaryButton
// region SecondaryButton
@Composable
fun SecondaryButton(
text: String,
@ -229,6 +235,29 @@ fun SecondaryButtonIconLeft(
showProgress = showProgress,
)
}
// endregion SecondaryButton
// region Other
@Composable
fun SelectorButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
TangemButton(
modifier = modifier,
text = text,
textStyle = TangemTheme.typography.subtitle2,
icon = TangemButtonIcon.Right(painterResource(id = R.drawable.ic_chevron_24)),
onClick = onClick,
colors = TangemButtonsDefaults.selectorButtonColors,
showProgress = false,
enabled = enabled,
size = TangemButtonSize.Selector,
)
}
// endregion Other
// region Defaults
@Suppress("LongParameterList")
@ -243,12 +272,12 @@ private fun TangemButton(
modifier: Modifier = Modifier,
size: TangemButtonSize = TangemButtonSize.Default,
elevation: ButtonElevation = TangemButtonsDefaults.elevation,
textStyle: TextStyle = TangemTheme.typography.button,
) {
Button(
modifier = modifier
.width(IntrinsicSize.Min)
.height(IntrinsicSize.Min)
.heightIn(size.toHeightDp()),
.heightIn(min = size.toHeightDp()),
onClick = {
if (!showProgress) {
onClick()
@ -261,19 +290,24 @@ private fun TangemButton(
) {
ButtonContent(
text = text,
textStyle = textStyle,
buttonIcon = icon,
colors = colors,
showProgress = showProgress,
enabled = enabled,
size = size,
)
}
}
@Suppress("LongParameterList")
@Composable
private fun ButtonContent(
text: String,
textStyle: TextStyle,
buttonIcon: TangemButtonIcon,
colors: ButtonColors,
size: TangemButtonSize,
enabled: Boolean,
showProgress: Boolean,
) {
@ -297,18 +331,19 @@ private fun ButtonContent(
)
}
} else {
Row {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(size.toIconPadding()),
) {
if (buttonIcon is TangemButtonIcon.Left) {
icon(buttonIcon.painter)
Spacer(modifier = Modifier.width(TangemTheme.dimens.size8))
}
Text(
text = text,
style = TangemTheme.typography.button,
style = textStyle,
color = colors.contentColor(enabled = enabled).value,
)
if (buttonIcon is TangemButtonIcon.Right) {
Spacer(modifier = Modifier.width(TangemTheme.dimens.size8))
icon(buttonIcon.painter)
}
}
@ -332,18 +367,28 @@ sealed interface TangemButtonIcon {
enum class TangemButtonSize {
Default,
Text,
Selector,
}
@Composable
private fun TangemButtonSize.toHeightDp(): Dp = when (this) {
TangemButtonSize.Default -> TangemTheme.dimens.size48
TangemButtonSize.Text -> TangemTheme.dimens.size40
TangemButtonSize.Selector -> TangemTheme.dimens.size24
}
@Composable
private fun TangemButtonSize.toShape(): Shape = when (this) {
TangemButtonSize.Default -> TangemTheme.shapes.roundedCornersMedium
TangemButtonSize.Text -> TangemTheme.shapes.roundedCornersSmall
TangemButtonSize.Selector -> TangemTheme.shapes.roundedCornersSmall
}
@Composable
private fun TangemButtonSize.toIconPadding(): Dp = when (this) {
TangemButtonSize.Default -> TangemTheme.dimens.spacing8
TangemButtonSize.Text -> TangemTheme.dimens.spacing8
TangemButtonSize.Selector -> 0.dp
}
object TangemButtonsDefaults {
@ -385,6 +430,14 @@ object TangemButtonsDefaults {
disabledBackgroundColor = Color.Transparent,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val selectorButtonColors: ButtonColors
@Composable get() = TangemButtonColors(
backgroundColor = Color.Transparent,
contentColor = TangemTheme.colors.text.tertiary,
disabledBackgroundColor = Color.Transparent,
disabledContentColor = TangemTheme.colors.text.disabled,
)
}
@Immutable
@ -595,6 +648,11 @@ private fun TextButtonSample(
text = "Delete",
onClick = { /* no-op */ },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
SelectorButton(
text = "USD",
onClick = { /* no-op */ },
)
}
}

View file

@ -50,7 +50,7 @@ fun BigDecimal.toFiatString(
val formatter = NumberFormat.getInstance(Locale.getDefault()) as? DecimalFormat
val df = formatter?.apply {
maximumFractionDigits = 2
minimumFractionDigits = 0
minimumFractionDigits = 2
isGroupingUsed = true
this.roundingMode = roundingMode
}

View file

@ -52,7 +52,7 @@ internal class SwapInteractorImpl @Inject constructor(
// replace tokens in wallet tokens list with loaded same
val loadedOnWalletsMap = mutableSetOf<String>()
val tokensInWallet = userWalletManager.getUserTokens(networkId, false)
val tokensInWallet = userWalletManager.getUserTokens(networkId = networkId, isExcludeCustom = true)
.filter { it.symbol != initialCurrency.symbol }
.map { token ->
allLoadedTokens.firstOrNull { it.symbol == token.symbol }?.let {
@ -68,17 +68,17 @@ internal class SwapInteractorImpl @Inject constructor(
.mapValues { SwapAmount(it.value.value, it.value.decimals) }
val appCurrency = userWalletManager.getUserAppCurrency()
val rates = repository.getRates(appCurrency.code, tokensInWallet.map { it.id })
cache.cacheLoadedTokens(loadedTokens)
cache.cacheBalances(tokensBalance)
cache.cacheInWalletTokens(tokensInWallet)
cache.cacheLoadedTokens(loadedTokens.map { TokenWithBalance(it) })
cache.cacheInWalletTokens(getTokensWithBalance(tokensInWallet, tokensBalance, rates, appCurrency))
return TokensDataState(
preselectTokens = PreselectTokens(
fromToken = initialCurrency,
toToken = selectToToken(initialCurrency, tokensInWallet, loadedTokens),
),
foundTokensState = FoundTokensState(
tokensInWallet = getTokensWithBalance(tokensInWallet, tokensBalance, rates, appCurrency),
loadedTokens = loadedTokens.map { TokenWithBalance(it) },
tokensInWallet = cache.getInWalletTokens(),
loadedTokens = cache.getLoadedTokens(),
),
)
}
@ -87,28 +87,25 @@ internal class SwapInteractorImpl @Inject constructor(
val searchQueryLowerCase = searchQuery.lowercase()
val tokensInWallet = cache.getInWalletTokens()
.filter {
it.name.lowercase().contains(searchQueryLowerCase) ||
it.symbol.lowercase().contains(searchQueryLowerCase)
it.token.name.lowercase().contains(searchQueryLowerCase) ||
it.token.symbol.lowercase().contains(searchQueryLowerCase)
}
val loadedTokens = cache.getLoadedTokens()
.filter {
it.name.lowercase().contains(searchQueryLowerCase) ||
it.symbol.lowercase().contains(searchQueryLowerCase)
it.token.name.lowercase().contains(searchQueryLowerCase) ||
it.token.symbol.lowercase().contains(searchQueryLowerCase)
}
val tokensBalance = userWalletManager.getCurrentWalletTokensBalance(networkId, emptyList())
.mapValues { SwapAmount(it.value.value, it.value.decimals) }
val appCurrency = userWalletManager.getUserAppCurrency()
val rates = repository.getRates(appCurrency.code, tokensInWallet.map { it.id })
return FoundTokensState(
tokensInWallet = getTokensWithBalance(tokensInWallet, tokensBalance, rates, appCurrency),
loadedTokens = loadedTokens.map { TokenWithBalance(it) },
tokensInWallet = tokensInWallet,
loadedTokens = loadedTokens,
)
}
override fun findTokenById(id: String): Currency? {
val tokensInWallet = cache.getInWalletTokens()
val loadedTokens = cache.getLoadedTokens()
return tokensInWallet.firstOrNull { it.id == id } ?: loadedTokens.firstOrNull { it.id == id }
return tokensInWallet.firstOrNull { it.token.id == id }?.token
?: loadedTokens.firstOrNull { it.token.id == id }?.token
}
override suspend fun givePermissionToSwap(
@ -218,6 +215,7 @@ internal class SwapInteractorImpl @Inject constructor(
return when (result) {
is SendTxResult.Success -> {
userWalletManager.addToken(cryptoCurrencyConverter.convert(currencyToGet))
userWalletManager.refreshWallet()
TxState.TxSent(
fromAmount = amountFormatter.formatSwapAmountToUI(swapData.fromTokenAmount, currencyToSend.symbol),
toAmount = amountFormatter.formatSwapAmountToUI(swapData.toTokenAmount, currencyToGet.symbol),
@ -283,10 +281,13 @@ internal class SwapInteractorImpl @Inject constructor(
TokenWithBalance(
token = it,
tokenBalanceData = TokenBalanceData(
amount = balance?.let { amount -> amountFormatter.formatSwapAmountToUI(amount, "") },
amount = balance?.let { amount ->
amountFormatter.formatSwapAmountToUI(amount, it.symbol)
},
amountEquivalent = balance?.value?.toFiatString(
rates[it.id]?.toBigDecimal() ?: BigDecimal.ZERO,
appCurrency.symbol,
rateValue = rates[it.id]?.toBigDecimal() ?: BigDecimal.ZERO,
fiatCurrencyName = appCurrency.symbol,
formatWithSpaces = true,
),
),
)
@ -331,7 +332,11 @@ internal class SwapInteractorImpl @Inject constructor(
return SwapState.EmptyAmountState(
fromTokenWalletBalance = fromTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") }.orEmpty(),
toTokenWalletBalance = toTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") }.orEmpty(),
zeroAmountEquivalent = BigDecimal.ZERO.toFiatString(BigDecimal.ONE, appCurrency.symbol),
zeroAmountEquivalent = BigDecimal.ZERO.toFiatString(
rateValue = BigDecimal.ONE,
fiatCurrencyName = appCurrency.symbol,
formatWithSpaces = true,
),
)
}
@ -402,7 +407,7 @@ internal class SwapInteractorImpl @Inject constructor(
val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId)
val rates = repository.getRates(appCurrency.code, listOf(fromTokenId, toTokenId, nativeToken.id))
return rates[nativeToken.id]?.toBigDecimal()?.let { rate ->
" (${fee.toFiatString(rate, appCurrency.symbol)})"
" (${fee.toFiatString(rate, appCurrency.symbol, true)})"
}.orEmpty()
}
@ -482,8 +487,9 @@ internal class SwapInteractorImpl @Inject constructor(
tokenWalletBalance = fromTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") }
?: ZERO_BALANCE,
tokenFiatBalance = fromTokenAmount.value.toFiatString(
rates[fromToken.id]?.toBigDecimal() ?: BigDecimal.ZERO,
appCurrency.symbol,
rateValue = rates[fromToken.id]?.toBigDecimal() ?: BigDecimal.ZERO,
fiatCurrencyName = appCurrency.symbol,
formatWithSpaces = true,
),
),
toTokenInfo = TokenSwapInfo(
@ -492,8 +498,9 @@ internal class SwapInteractorImpl @Inject constructor(
tokenWalletBalance = toTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") }
?: ZERO_BALANCE,
tokenFiatBalance = toTokenAmount.value.toFiatString(
rates[toToken.id]?.toBigDecimal() ?: BigDecimal.ZERO,
appCurrency.symbol,
rateValue = rates[toToken.id]?.toBigDecimal() ?: BigDecimal.ZERO,
fiatCurrencyName = appCurrency.symbol,
formatWithSpaces = true,
),
),
fee = formattedFee,
@ -546,12 +553,15 @@ internal class SwapInteractorImpl @Inject constructor(
}
private suspend fun syncWalletBalanceForTokens(networkId: String, tokens: List<Currency>) {
val tokensBalance =
userWalletManager.getCurrentWalletTokensBalance(
networkId = networkId,
extraTokens = tokens.map { cryptoCurrencyConverter.convert(it) },
)
cache.cacheBalances(tokensBalance.mapValues { SwapAmount(it.value.value, it.value.decimals) })
val tokensToSync = tokens.filter { cache.getBalanceForToken(it.symbol) == null }
if (tokensToSync.isNotEmpty()) {
val tokensBalance =
userWalletManager.getCurrentWalletTokensBalance(
networkId = networkId,
extraTokens = tokensToSync.map { cryptoCurrencyConverter.convert(it) },
)
cache.cacheBalances(tokensBalance.mapValues { SwapAmount(it.value.value, it.value.decimals) })
}
}
private fun isBalanceEnough(fromToken: Currency, amount: SwapAmount, fee: BigDecimal?): Boolean {
@ -618,9 +628,9 @@ internal class SwapInteractorImpl @Inject constructor(
toTokenAmount: BigDecimal,
toRate: Double,
): Float {
val toTokenFiatValue = toTokenAmount.multiply(toRate.toBigDecimal()).setScale(2, RoundingMode.HALF_UP)
val fromTokenFiatValue = fromTokenAmount.multiply(fromRate.toBigDecimal()).setScale(2, RoundingMode.HALF_UP)
return (BigDecimal.ONE - toTokenFiatValue / fromTokenFiatValue).toFloat()
val toTokenFiatValue = toTokenAmount.multiply(toRate.toBigDecimal())
val fromTokenFiatValue = fromTokenAmount.multiply(fromRate.toBigDecimal())
return (BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP)).toFloat()
}
companion object {

View file

@ -2,18 +2,19 @@ package com.tangem.feature.swap.domain.cache
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.ui.TokenWithBalance
import java.math.BigDecimal
interface SwapDataCache {
fun cacheAvailableToSwapTokens(networkId: String, tokens: List<Currency>)
fun cacheInWalletTokens(tokens: List<Currency>)
fun cacheLoadedTokens(tokens: List<Currency>)
fun cacheInWalletTokens(tokens: List<TokenWithBalance>)
fun cacheLoadedTokens(tokens: List<TokenWithBalance>)
fun cacheBalances(balances: Map<String, SwapAmount>)
fun cacheLastFeeForNetwork(fee: BigDecimal, networkId: String)
fun getAvailableTokens(networkId: String): List<Currency>
fun getInWalletTokens(): List<Currency>
fun getLoadedTokens(): List<Currency>
fun getInWalletTokens(): List<TokenWithBalance>
fun getLoadedTokens(): List<TokenWithBalance>
fun getBalanceForToken(symbol: String): SwapAmount?
fun getLastFeeForNetwork(networkId: String): BigDecimal?
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.swap.domain.cache
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.ui.TokenWithBalance
import java.math.BigDecimal
class SwapDataCacheImpl : SwapDataCache {
@ -9,28 +10,28 @@ class SwapDataCacheImpl : SwapDataCache {
private val availableTokensForNetwork: MutableMap<String, List<Currency>> = mutableMapOf()
private val feesForNetworks: MutableMap<String, BigDecimal> = mutableMapOf()
private val tokensBalances: MutableMap<String, SwapAmount> = mutableMapOf()
private val lastInWalletTokens = mutableListOf<Currency>()
private val lastLoadedTokens = mutableListOf<Currency>()
private val lastInWalletTokens = mutableListOf<TokenWithBalance>()
private val lastLoadedTokens = mutableListOf<TokenWithBalance>()
override fun cacheLastFeeForNetwork(fee: BigDecimal, networkId: String) {
feesForNetworks[networkId] = fee
}
override fun cacheInWalletTokens(tokens: List<Currency>) {
override fun cacheInWalletTokens(tokens: List<TokenWithBalance>) {
lastInWalletTokens.clear()
lastInWalletTokens.addAll(tokens)
}
override fun cacheLoadedTokens(tokens: List<Currency>) {
override fun cacheLoadedTokens(tokens: List<TokenWithBalance>) {
lastLoadedTokens.clear()
lastLoadedTokens.addAll(tokens)
}
override fun getInWalletTokens(): List<Currency> {
override fun getInWalletTokens(): List<TokenWithBalance> {
return lastInWalletTokens
}
override fun getLoadedTokens(): List<Currency> {
override fun getLoadedTokens(): List<TokenWithBalance> {
return lastLoadedTokens
}

View file

@ -7,7 +7,6 @@ import android.view.ViewGroup
import androidx.compose.animation.Crossfade
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.tangem.feature.swap.router.CustomTabsManager
@ -26,7 +25,6 @@ class SwapFragment : Fragment() {
private val viewModel by viewModels<SwapViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, true) }
viewModel.setRouter(
SwapRouter(
fragmentManager = WeakReference(parentFragmentManager),

View file

@ -79,6 +79,8 @@ internal class StateBuilder(val actions: UiActions) {
toToken: Currency,
mainTokenId: String,
): SwapStateHolder {
val canSelectSendToken = mainTokenId != fromToken.id
val canSelectReceiveToken = mainTokenId != toToken.id
return uiStateHolder.copy(
sendCardData = SwapCardData(
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
@ -88,8 +90,8 @@ internal class StateBuilder(val actions: UiActions) {
tokenCurrency = fromToken.symbol,
coinId = fromToken.id,
isNotNativeToken = fromToken.isNonNative(),
canSelectAnotherToken = mainTokenId != fromToken.id,
balance = "",
canSelectAnotherToken = canSelectSendToken,
balance = if (!canSelectSendToken) uiStateHolder.sendCardData.balance else "",
),
receiveCardData = SwapCardData(
type = TransactionCardType.ReceiveCard(),
@ -99,8 +101,8 @@ internal class StateBuilder(val actions: UiActions) {
tokenCurrency = toToken.symbol,
coinId = toToken.id,
isNotNativeToken = toToken.isNonNative(),
canSelectAnotherToken = mainTokenId != toToken.id,
balance = "",
canSelectAnotherToken = canSelectReceiveToken,
balance = if (!canSelectReceiveToken) uiStateHolder.receiveCardData.balance else "",
),
fee = FeeState.Loading,
swapButton = SwapButton(enabled = false, loading = true, onClick = {}),
@ -198,6 +200,7 @@ internal class StateBuilder(val actions: UiActions) {
canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken,
balance = emptyAmountState.toTokenWalletBalance,
),
warnings = emptyList(),
fee = FeeState.Empty,
swapButton = SwapButton(
enabled = false,

View file

@ -3,6 +3,7 @@ package com.tangem.feature.swap.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetLayout
@ -39,6 +40,7 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) {
TangemTheme {
ModalBottomSheetLayout(
modifier = Modifier.systemBarsPadding(),
sheetContent = {
if (stateHolder.permissionState is SwapPermissionState.ReadyForRequest) {
SwapPermissionBottomSheetContent(

View file

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
@ -37,6 +38,7 @@ import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.PrimaryButtonIconRight
import com.tangem.core.ui.components.RefreshableWaringCard
import com.tangem.core.ui.components.SimpleOkDialog
import com.tangem.core.ui.components.SmallInfoCard
import com.tangem.core.ui.components.SmallInfoCardWithDisclaimer
import com.tangem.core.ui.components.SmallInfoCardWithWarning
import com.tangem.core.ui.components.WarningCard
@ -120,7 +122,9 @@ internal fun SwapScreenContent(state: SwapStateHolder, onPermissionWarningClick:
AnimatedVisibility(
visible = keyboard is Keyboard.Opened,
modifier = Modifier.align(Alignment.BottomCenter),
modifier = Modifier
.imePadding()
.align(Alignment.BottomCenter),
) {
Text(
text = stringResource(id = R.string.send_max_amount_label),
@ -277,7 +281,9 @@ private fun FeeItem(feeState: FeeState, currency: String) {
),
)
}
is FeeState.Empty -> {}
is FeeState.Empty -> {
SmallInfoCard(startText = titleString, endText = "")
}
}
}

View file

@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
@ -51,6 +52,9 @@ fun SwapSelectTokenScreen(
) {
TangemTheme {
Scaffold(
modifier = Modifier
.systemBarsPadding()
.background(color = TangemTheme.colors.background.secondary),
content = { padding ->
ListOfTokens(state = state, Modifier.padding(padding))
},
@ -66,7 +70,6 @@ fun SwapSelectTokenScreen(
icon = painterResource(id = getActiveIconRes(state.network.blockchainId)),
)
},
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
)
}
}
@ -178,13 +181,13 @@ private fun TokenItem(token: TokenToSelect, network: Network, onTokenClick: () -
modifier = Modifier.padding(start = TangemTheme.dimens.spacing8),
) {
Text(
text = token.addedTokenBalanceData.amount ?: "",
text = token.addedTokenBalanceData.amountEquivalent.orEmpty(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
SpacerW2()
Text(
text = token.addedTokenBalanceData.amountEquivalent ?: "",
text = token.addedTokenBalanceData.amount.orEmpty(),
style = TangemTheme.typography.caption,
color = TangemTheme.colors.text.tertiary,
)

View file

@ -1,6 +1,7 @@
package com.tangem.feature.swap.ui
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -19,6 +20,7 @@ import com.tangem.feature.swap.presentation.R
fun SwapSuccessScreen(state: SwapSuccessStateHolder, onBack: () -> Unit) {
TangemTheme {
Scaffold(
modifier = Modifier.systemBarsPadding(),
content = { padding ->
ResultScreenContent(
resultMessage = makeSuccessMessage(

View file

@ -251,6 +251,9 @@ internal class SwapViewModel @Inject constructor(
analyticsEventHandler.send(SwapEvents.SwapInProgressScreen)
swapRouter.openScreen(SwapNavScreen.Success)
}
is TxState.UserCancelled -> {
startLoadingQuotesFromLastState()
}
else -> {
startLoadingQuotesFromLastState()
uiState = stateBuilder.createErrorTransaction(uiState, it) {
@ -282,6 +285,9 @@ internal class SwapViewModel @Inject constructor(
is TxState.TxSent -> {
uiState = stateBuilder.loadingPermissionState(uiState)
}
is TxState.UserCancelled -> {
/* no-op */
}
else -> {
uiState = stateBuilder.createErrorTransaction(uiState, it) {
uiState = stateBuilder.clearAlert(uiState)

View file

@ -39,6 +39,8 @@ interface UserWalletManager {
@Throws(IllegalStateException::class)
suspend fun addToken(currency: Currency)
fun refreshWallet()
/**
* Returns wallet public address for token
*