diff --git a/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt
index 45044583d2..bc33cbeb88 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt
@@ -1,6 +1,7 @@
package com.tangem.tap.di.domain
import com.tangem.domain.account.supplier.MultiAccountListSupplier
+import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
@@ -39,10 +40,12 @@ internal object QrScanningDomainModule {
fun provideResolveQrSendTargetsUseCase(
multiAccountListSupplier: MultiAccountListSupplier,
qrScanningEventsRepository: QrScanningEventsRepository,
+ userWalletsListRepository: UserWalletsListRepository,
): ResolveQrSendTargetsUseCase {
return ResolveQrSendTargetsUseCase(
multiAccountListSupplier = multiAccountListSupplier,
qrScanningEventsRepository = qrScanningEventsRepository,
+ userWalletsListRepository = userWalletsListRepository,
)
}
}
\ No newline at end of file
diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index f60477cf1c..020150535e 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -217,6 +217,7 @@
Add
Add to portfolio
Add token
+ Add tokens
Added
Address
All
@@ -325,6 +326,7 @@
NFT
No
No address
+ No results
Not Added
Not available
Not now
@@ -383,6 +385,7 @@
To
To %s
Today
+ Token to send
- %d token
- %d tokens
@@ -395,6 +398,7 @@
I understand
I understand, continue
There was an error. Please try again.
+ Unlock
Unreachable
Unstake
Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount.
@@ -1211,6 +1215,10 @@
Memo: %s
Invalid Memo
Network fee coverage
+
+ - %d token isn\'t compatible with this address
+ - %d tokens aren\'t compatible with this address
+
Nonce
Unique number for each transaction. Use it to resend or cancel a pending transaction.
Enter nonceā¦
diff --git a/domain/qr-scanning/build.gradle.kts b/domain/qr-scanning/build.gradle.kts
index 564ec92b53..2679da7164 100644
--- a/domain/qr-scanning/build.gradle.kts
+++ b/domain/qr-scanning/build.gradle.kts
@@ -13,6 +13,7 @@ dependencies {
/** Domain */
api(projects.domain.models)
implementation(projects.domain.account)
+ implementation(projects.domain.common)
implementation(projects.domain.qrScanning.models)
implementation(projects.domain.tokens.models)
diff --git a/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrSendTarget.kt b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrSendTarget.kt
index d802a5f2ec..d0443ffdd7 100644
--- a/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrSendTarget.kt
+++ b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrSendTarget.kt
@@ -35,6 +35,7 @@ sealed class QrSendTarget {
val accountId: AccountId,
val accountName: AccountName,
val currencies: List,
+ val hiddenTokensCount: Int = 0,
)
}
diff --git a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ResolveQrSendTargetsUseCase.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ResolveQrSendTargetsUseCase.kt
index 2d436c4ceb..2e5cea6b3a 100644
--- a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ResolveQrSendTargetsUseCase.kt
+++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ResolveQrSendTargetsUseCase.kt
@@ -6,6 +6,7 @@ import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
+import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.qrscanning.models.ClassifiedQrContent
import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
import java.math.BigDecimal
@@ -14,41 +15,41 @@ import com.tangem.domain.qrscanning.models.QrSendTarget
class ResolveQrSendTargetsUseCase(
private val multiAccountListSupplier: MultiAccountListSupplier,
private val qrScanningEventsRepository: QrScanningEventsRepository,
+ private val userWalletsListRepository: UserWalletsListRepository,
) {
suspend operator fun invoke(qrCode: String): QrSendTarget {
val allAccountLists = multiAccountListSupplier.getSyncOrNull(Unit).orEmpty()
+ val userWallets = userWalletsListRepository.userWalletsSync()
+ val walletNamesMap = userWallets.associate { it.walletId to it.name }
- val currencyEntries = allAccountLists.flatMap { accountList ->
- accountList.accounts
- .filterIsInstance()
- .flatMap { account ->
- account.cryptoCurrencies.map { currency ->
- currency to CurrencyLocation(
- userWalletId = accountList.userWalletId,
- walletName = accountList.userWalletId.stringValue,
- accountId = account.accountId,
- accountName = account.accountName,
- )
- }
+ val allCurrencies = mutableListOf()
+ val currencyLocations = mutableMapOf>()
+ val totalPerAccount = mutableMapOf()
+
+ for (accountList in allAccountLists) {
+ for (account in accountList.accounts.filterIsInstance()) {
+ val location = CurrencyLocation(
+ walletName = walletNamesMap[account.accountId.userWalletId]
+ ?: account.accountId.userWalletId.stringValue,
+ accountId = account.accountId,
+ accountName = account.accountName,
+ )
+ totalPerAccount[account.accountId] = account.cryptoCurrencies.size
+ for (currency in account.cryptoCurrencies) {
+ allCurrencies.add(currency)
+ currencyLocations.getOrPut(currency.id) { mutableListOf() }.add(location)
}
+ }
}
- val allCurrencies = currencyEntries.map { it.first }
- val currencyLocations = currencyEntries.groupBy(
- keySelector = { it.first.id },
- valueTransform = { it.second },
- )
-
val classified = qrScanningEventsRepository.classify(qrCode, allCurrencies)
+ val portfolioIndex = PortfolioIndex(currencyLocations, totalPerAccount)
- return resolve(classified, currencyLocations)
+ return resolve(classified, portfolioIndex)
}
- private fun resolve(
- classified: ClassifiedQrContent,
- currencyLocations: Map>,
- ): QrSendTarget {
+ private fun resolve(classified: ClassifiedQrContent, portfolioIndex: PortfolioIndex): QrSendTarget {
return when (classified) {
is ClassifiedQrContent.WalletConnect -> QrSendTarget.WalletConnect(classified.uri)
is ClassifiedQrContent.Unknown -> QrSendTarget.Unknown(classified.raw)
@@ -57,14 +58,14 @@ class ResolveQrSendTargetsUseCase(
amount = null,
memo = null,
matchingCurrencies = classified.matchingCurrencies,
- currencyLocations = currencyLocations,
+ portfolioIndex = portfolioIndex,
)
is ClassifiedQrContent.PaymentUri -> resolveAddressTarget(
address = classified.address,
amount = classified.amount,
memo = classified.memo,
matchingCurrencies = classified.matchingCurrencies,
- currencyLocations = currencyLocations,
+ portfolioIndex = portfolioIndex,
)
}
}
@@ -74,9 +75,9 @@ class ResolveQrSendTargetsUseCase(
amount: BigDecimal?,
memo: String?,
matchingCurrencies: List,
- currencyLocations: Map>,
+ portfolioIndex: PortfolioIndex,
): QrSendTarget {
- val walletGroups = buildWalletGroups(matchingCurrencies, currencyLocations)
+ val walletGroups = buildWalletGroups(matchingCurrencies, portfolioIndex)
val singleGroup = walletGroups.singleOrNull()
val singleCurrency = singleGroup?.accounts?.singleOrNull()?.currencies?.singleOrNull()
@@ -101,15 +102,15 @@ class ResolveQrSendTargetsUseCase(
private fun buildWalletGroups(
matchingCurrencies: List,
- currencyLocations: Map>,
+ portfolioIndex: PortfolioIndex,
): List {
val walletMap = linkedMapOf()
val uniqueCurrencies = matchingCurrencies.distinctBy { it.id }
for (currency in uniqueCurrencies) {
- val locations = currencyLocations[currency.id] ?: continue
+ val locations = portfolioIndex.currencyLocations[currency.id].orEmpty()
for (location in locations) {
- val walletInfo = walletMap.getOrPut(location.userWalletId) {
+ val walletInfo = walletMap.getOrPut(location.accountId.userWalletId) {
WalletInfo(location.walletName, linkedMapOf())
}
val accountInfo = walletInfo.accounts.getOrPut(location.accountId) {
@@ -124,18 +125,24 @@ class ResolveQrSendTargetsUseCase(
userWalletId = walletId,
walletName = walletInfo.walletName,
accounts = walletInfo.accounts.map { (accountId, accountInfo) ->
+ val total = portfolioIndex.totalPerAccount[accountId] ?: 0
QrSendTarget.Multiple.AccountGroup(
accountId = accountId,
accountName = accountInfo.accountName,
currencies = accountInfo.currencies,
+ hiddenTokensCount = total - accountInfo.currencies.size,
)
},
)
}
}
+ private class PortfolioIndex(
+ val currencyLocations: Map>,
+ val totalPerAccount: Map,
+ )
+
private data class CurrencyLocation(
- val userWalletId: UserWalletId,
val walletName: String,
val accountId: AccountId,
val accountName: AccountName,
diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/NetworkSelectionComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/NetworkSelectionComponent.kt
new file mode 100644
index 0000000000..3214424d10
--- /dev/null
+++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/NetworkSelectionComponent.kt
@@ -0,0 +1,36 @@
+package com.tangem.features.send.v2.api
+
+import com.tangem.core.decompose.factory.ComponentFactory
+import com.tangem.core.ui.decompose.ComposableDialogComponent
+import com.tangem.domain.models.account.AccountId
+import com.tangem.domain.models.account.AccountName
+import com.tangem.domain.models.currency.CryptoCurrency
+import com.tangem.domain.models.wallet.UserWalletId
+import java.math.BigDecimal
+
+interface NetworkSelectionComponent : ComposableDialogComponent {
+
+ data class Params(
+ val address: String,
+ val amount: BigDecimal?,
+ val memo: String?,
+ val walletGroups: List,
+ val onTokenSelected: (UserWalletId, CryptoCurrency) -> Unit,
+ val onDismiss: () -> Unit,
+ ) {
+ data class WalletGroup(
+ val userWalletId: UserWalletId,
+ val walletName: String,
+ val accounts: List,
+ )
+
+ data class AccountGroup(
+ val accountId: AccountId,
+ val accountName: AccountName,
+ val currencies: List,
+ val hiddenTokensCount: Int = 0,
+ )
+ }
+
+ interface Factory : ComponentFactory
+}
\ No newline at end of file
diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/networkselection/DefaultNetworkSelectionComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/networkselection/DefaultNetworkSelectionComponent.kt
new file mode 100644
index 0000000000..a154ed6312
--- /dev/null
+++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/networkselection/DefaultNetworkSelectionComponent.kt
@@ -0,0 +1,38 @@
+package com.tangem.features.send.v2.networkselection
+
+import androidx.compose.runtime.Composable
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.tangem.core.decompose.context.AppComponentContext
+import com.tangem.core.decompose.model.getOrCreateModel
+import com.tangem.features.send.v2.api.NetworkSelectionComponent
+import com.tangem.features.send.v2.networkselection.model.NetworkSelectionModel
+import com.tangem.features.send.v2.networkselection.ui.NetworkSelectionScreen
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+internal class DefaultNetworkSelectionComponent @AssistedInject constructor(
+ @Assisted context: AppComponentContext,
+ @Assisted private val params: NetworkSelectionComponent.Params,
+) : NetworkSelectionComponent, AppComponentContext by context {
+
+ private val model: NetworkSelectionModel = getOrCreateModel(params)
+
+ override fun dismiss() {
+ params.onDismiss()
+ }
+
+ @Composable
+ override fun Dialog() {
+ val state = model.uiState.collectAsStateWithLifecycle()
+ NetworkSelectionScreen(state = state.value, onDismiss = ::dismiss)
+ }
+
+ @AssistedFactory
+ interface Factory : NetworkSelectionComponent.Factory {
+ override fun create(
+ context: AppComponentContext,
+ params: NetworkSelectionComponent.Params,
+ ): DefaultNetworkSelectionComponent
+ }
+}
\ No newline at end of file
diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/networkselection/di/NetworkSelectionFeatureModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/networkselection/di/NetworkSelectionFeatureModule.kt
new file mode 100644
index 0000000000..8c099c5bfa
--- /dev/null
+++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/networkselection/di/NetworkSelectionFeatureModule.kt
@@ -0,0 +1,20 @@
+package com.tangem.features.send.v2.networkselection.di
+
+import com.tangem.features.send.v2.api.NetworkSelectionComponent
+import com.tangem.features.send.v2.networkselection.DefaultNetworkSelectionComponent
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal interface NetworkSelectionFeatureModule {
+
+ @Binds
+ @Singleton
+ fun bindNetworkSelectionComponentFactory(
+ impl: DefaultNetworkSelectionComponent.Factory,
+ ): NetworkSelectionComponent.Factory
+}
\ No newline at end of file
diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/networkselection/di/NetworkSelectionModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/networkselection/di/NetworkSelectionModelModule.kt
new file mode 100644
index 0000000000..e1c8e50691
--- /dev/null
+++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/networkselection/di/NetworkSelectionModelModule.kt
@@ -0,0 +1,20 @@
+package com.tangem.features.send.v2.networkselection.di
+
+import com.tangem.core.decompose.di.ModelComponent
+import com.tangem.core.decompose.model.Model
+import com.tangem.features.send.v2.networkselection.model.NetworkSelectionModel
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+@InstallIn(ModelComponent::class)
+internal interface NetworkSelectionModelModule {
+
+ @Binds
+ @IntoMap
+ @ClassKey(NetworkSelectionModel::class)
+ fun provideNetworkSelectionModel(model: NetworkSelectionModel): Model
+}
\ No newline at end of file
diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/networkselection/entity/NetworkSelectionUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/networkselection/entity/NetworkSelectionUM.kt
new file mode 100644
index 0000000000..680e6b27bf
--- /dev/null
+++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/networkselection/entity/NetworkSelectionUM.kt
@@ -0,0 +1,33 @@
+package com.tangem.features.send.v2.networkselection.entity
+
+import androidx.compose.runtime.Immutable
+import com.tangem.core.ui.components.currency.icon.CurrencyIconState
+import com.tangem.core.ui.components.fields.entity.SearchBarUM
+import com.tangem.core.ui.components.token.state.TokenItemState
+import com.tangem.core.ui.extensions.TextReference
+import com.tangem.domain.models.wallet.UserWalletId
+import kotlinx.collections.immutable.ImmutableList
+
+@Immutable
+internal data class NetworkSelectionUM(
+ val searchBar: SearchBarUM,
+ val walletGroups: ImmutableList,
+ val isBalanceHidden: Boolean,
+)
+
+@Immutable
+internal data class WalletGroupUM(
+ val userWalletId: UserWalletId,
+ val walletName: String,
+ val isExpanded: Boolean,
+ val onExpandToggle: () -> Unit,
+ val accounts: ImmutableList,
+)
+
+@Immutable
+internal data class AccountGroupUM(
+ val accountName: TextReference,
+ val iconState: CurrencyIconState.CryptoPortfolio?,
+ val tokens: ImmutableList,
+ val hiddenTokensCount: Int,
+)
\ No newline at end of file
diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/networkselection/model/NetworkSelectionModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/networkselection/model/NetworkSelectionModel.kt
new file mode 100644
index 0000000000..1f937110c0
--- /dev/null
+++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/networkselection/model/NetworkSelectionModel.kt
@@ -0,0 +1,264 @@
+package com.tangem.features.send.v2.networkselection.model
+
+import androidx.compose.runtime.Stable
+import com.tangem.common.getTotalCryptoAmount
+import com.tangem.common.getTotalFiatAmount
+import com.tangem.core.decompose.di.ModelScoped
+import com.tangem.core.decompose.model.Model
+import com.tangem.core.decompose.model.ParamsContainer
+import com.tangem.common.ui.account.AccountIconItemStateConverter
+import com.tangem.core.ui.components.account.AccountIconSize
+import com.tangem.core.ui.components.currency.icon.CurrencyIconState
+import com.tangem.core.ui.components.currency.icon.CurrencyIconStateBuilder
+import com.tangem.core.ui.components.fields.entity.SearchBarUM
+import com.tangem.core.ui.components.token.state.TokenItemState
+import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.core.ui.extensions.stringReference
+import com.tangem.core.ui.format.bigdecimal.crypto
+import com.tangem.core.ui.format.bigdecimal.fiat
+import com.tangem.core.ui.format.bigdecimal.format
+import com.tangem.domain.account.models.AccountStatusList
+import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
+import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
+import com.tangem.domain.appcurrency.model.AppCurrency
+import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
+import com.tangem.domain.models.StatusSource
+import com.tangem.common.ui.account.toUM
+import com.tangem.domain.models.account.AccountId
+import com.tangem.domain.models.account.filterCryptoPortfolio
+import com.tangem.domain.models.currency.CryptoCurrency
+import com.tangem.domain.models.currency.CryptoCurrencyStatus
+import com.tangem.domain.models.wallet.UserWalletId
+import com.tangem.features.send.v2.api.NetworkSelectionComponent
+import com.tangem.features.send.v2.networkselection.entity.AccountGroupUM
+import com.tangem.features.send.v2.networkselection.entity.NetworkSelectionUM
+import com.tangem.features.send.v2.networkselection.entity.WalletGroupUM
+import com.tangem.utils.coroutines.CoroutineDispatcherProvider
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.flow.*
+import javax.inject.Inject
+
+@Stable
+@ModelScoped
+internal class NetworkSelectionModel @Inject constructor(
+ paramsContainer: ParamsContainer,
+ override val dispatchers: CoroutineDispatcherProvider,
+ private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
+ private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
+ private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
+) : Model() {
+
+ private val params: NetworkSelectionComponent.Params = paramsContainer.require()
+
+ private val searchQuery = MutableStateFlow("")
+ private val expandedWallets = MutableStateFlow(
+ params.walletGroups.firstOrNull()
+ ?.userWalletId
+ ?.let(::setOf)
+ .orEmpty(),
+ )
+
+ val uiState: StateFlow = createUiStateFlow()
+
+ private fun createUiStateFlow(): StateFlow {
+ val appCurrencyFlow = getSelectedAppCurrencyUseCase.invokeOrDefault()
+ val statusListFlow = multiAccountStatusListSupplier()
+ val balanceHidingFlow = getBalanceHidingSettingsUseCase()
+
+ return combine(
+ flow = searchQuery,
+ flow2 = expandedWallets,
+ flow3 = appCurrencyFlow,
+ flow4 = statusListFlow,
+ flow5 = balanceHidingFlow,
+ ) { query, expanded, appCurrency, statusLists, balanceHidingSettings ->
+ buildState(
+ query = query,
+ expandedWallets = expanded,
+ appCurrency = appCurrency,
+ statusLists = statusLists,
+ isBalanceHidden = balanceHidingSettings.isBalanceHidden,
+ )
+ }.stateIn(
+ scope = modelScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = buildInitialState(),
+ )
+ }
+
+ private fun buildState(
+ query: String,
+ expandedWallets: Set,
+ appCurrency: AppCurrency,
+ statusLists: List,
+ isBalanceHidden: Boolean,
+ ): NetworkSelectionUM {
+ val walletGroups = params.walletGroups.mapNotNull { walletGroup ->
+ buildWalletGroup(
+ walletGroup = walletGroup,
+ query = query,
+ expandedWallets = expandedWallets,
+ appCurrency = appCurrency,
+ statusLists = statusLists,
+ )
+ }.toImmutableList()
+
+ return NetworkSelectionUM(
+ searchBar = createSearchBar(query),
+ walletGroups = walletGroups,
+ isBalanceHidden = isBalanceHidden,
+ )
+ }
+
+ private fun buildWalletGroup(
+ walletGroup: NetworkSelectionComponent.Params.WalletGroup,
+ query: String,
+ expandedWallets: Set,
+ appCurrency: AppCurrency,
+ statusLists: List,
+ ): WalletGroupUM? {
+ val statusList = statusLists.find { it.userWalletId == walletGroup.userWalletId }
+ val tokenBuildContext = TokenMappingParams(
+ userWalletId = walletGroup.userWalletId,
+ appCurrency = appCurrency,
+ statusMap = buildStatusMap(statusList),
+ )
+
+ val accounts = walletGroup.accounts.mapNotNull { accountGroup ->
+ val iconState = getAccountIcon(accountGroup.accountId, statusList)
+ buildAccountGroup(
+ accountGroup = accountGroup,
+ query = query,
+ context = tokenBuildContext,
+ iconState = iconState,
+ )
+ }.toImmutableList()
+
+ if (accounts.isEmpty()) return null
+
+ return WalletGroupUM(
+ userWalletId = walletGroup.userWalletId,
+ walletName = walletGroup.walletName,
+ isExpanded = walletGroup.userWalletId in expandedWallets,
+ onExpandToggle = { toggleWalletExpanded(walletGroup.userWalletId) },
+ accounts = accounts,
+ )
+ }
+
+ private fun buildStatusMap(statusList: AccountStatusList?): Map {
+ if (statusList == null) return emptyMap()
+ return statusList.accountStatuses
+ .filterCryptoPortfolio()
+ .flatMap { it.flattenCurrencies() }
+ .associateBy { it.currency.id }
+ }
+
+ private fun buildAccountGroup(
+ accountGroup: NetworkSelectionComponent.Params.AccountGroup,
+ query: String,
+ context: TokenMappingParams,
+ iconState: CurrencyIconState.CryptoPortfolio?,
+ ): AccountGroupUM? {
+ val tokens = accountGroup.currencies
+ .filter { matchesQuery(it, query) }
+ .map { currency -> buildTokenItem(currency, context) }
+ .toImmutableList()
+
+ if (tokens.isEmpty()) return null
+
+ return AccountGroupUM(
+ accountName = accountGroup.accountName.toUM().value,
+ iconState = iconState,
+ tokens = tokens,
+ hiddenTokensCount = accountGroup.hiddenTokensCount,
+ )
+ }
+
+ private fun getAccountIcon(
+ accountId: AccountId,
+ statusList: AccountStatusList?,
+ ): CurrencyIconState.CryptoPortfolio? {
+ if (statusList == null) return null
+ val account = statusList.accountStatuses
+ .filterCryptoPortfolio()
+ .find { it.accountId == accountId }
+ ?.account ?: return null
+ return AccountIconItemStateConverter(size = AccountIconSize.ExtraSmall).convert(account)
+ }
+
+ private fun matchesQuery(currency: CryptoCurrency, query: String): Boolean {
+ if (query.isBlank()) return true
+ val lowerQuery = query.lowercase()
+ return currency.name.lowercase().contains(lowerQuery) ||
+ currency.symbol.lowercase().contains(lowerQuery) ||
+ currency.network.name.lowercase().contains(lowerQuery)
+ }
+
+ private fun buildTokenItem(currency: CryptoCurrency, context: TokenMappingParams): TokenItemState {
+ val status = context.statusMap[currency.id]
+ if (status == null) {
+ return TokenItemState.Loading(id = currency.id.value)
+ }
+
+ val cryptoAmount = status.getTotalCryptoAmount()
+ val fiatAmount = status.getTotalFiatAmount()
+ val isFlickering = status.value.sources.total == StatusSource.CACHE
+
+ return TokenItemState.Content(
+ id = currency.id.value,
+ iconState = CurrencyIconStateBuilder.build(currency),
+ titleState = TokenItemState.TitleState.Content(
+ text = stringReference(currency.name),
+ ),
+ subtitleState = TokenItemState.SubtitleState.TextContent(
+ value = stringReference(currency.network.name),
+ ),
+ fiatAmountState = TokenItemState.FiatAmountState.Content(
+ text = fiatAmount.format {
+ fiat(
+ fiatCurrencyCode = context.appCurrency.code,
+ fiatCurrencySymbol = context.appCurrency.symbol,
+ )
+ },
+ isFlickering = isFlickering,
+ ),
+ subtitle2State = TokenItemState.Subtitle2State.TextContent(
+ text = cryptoAmount.format { crypto(currency) },
+ isFlickering = isFlickering,
+ ),
+ onItemClick = { params.onTokenSelected(context.userWalletId, currency) },
+ onItemLongClick = null,
+ )
+ }
+
+ private fun toggleWalletExpanded(walletId: UserWalletId) {
+ expandedWallets.update { current ->
+ if (walletId in current) current - walletId else current + walletId
+ }
+ }
+
+ private fun createSearchBar(query: String): SearchBarUM {
+ return SearchBarUM(
+ placeholderText = resourceReference(com.tangem.core.ui.R.string.common_search_tokens),
+ query = query,
+ onQueryChange = { searchQuery.value = it },
+ isActive = query.isNotEmpty(),
+ onActiveChange = {},
+ )
+ }
+
+ private fun buildInitialState(): NetworkSelectionUM {
+ return NetworkSelectionUM(
+ searchBar = createSearchBar(""),
+ walletGroups = persistentListOf(),
+ isBalanceHidden = true,
+ )
+ }
+
+ private data class TokenMappingParams(
+ val userWalletId: UserWalletId,
+ val appCurrency: AppCurrency,
+ val statusMap: Map,
+ )
+}
\ No newline at end of file
diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/networkselection/ui/NetworkSelectionScreen.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/networkselection/ui/NetworkSelectionScreen.kt
new file mode 100644
index 0000000000..aae3e17376
--- /dev/null
+++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/networkselection/ui/NetworkSelectionScreen.kt
@@ -0,0 +1,300 @@
+package com.tangem.features.send.v2.networkselection.ui
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import androidx.compose.ui.unit.dp
+import com.tangem.core.ui.R
+import com.tangem.core.ui.components.appbar.TangemTopAppBar
+import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
+import com.tangem.core.ui.components.currency.icon.CurrencyIcon
+import com.tangem.core.ui.components.fields.SearchBar
+import com.tangem.core.ui.components.token.TokenItem
+import com.tangem.core.ui.decorations.roundedShapeItemDecoration
+import com.tangem.core.ui.extensions.pluralStringResourceSafe
+import com.tangem.core.ui.extensions.resolveReference
+import com.tangem.core.ui.extensions.stringResourceSafe
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.features.send.v2.networkselection.entity.AccountGroupUM
+import com.tangem.features.send.v2.networkselection.entity.NetworkSelectionUM
+import com.tangem.features.send.v2.networkselection.entity.WalletGroupUM
+
+private const val CHEVRON_EXPANDED_ROTATION = 180f
+private const val CHEVRON_COLLAPSED_ROTATION = 0f
+
+@Composable
+internal fun NetworkSelectionScreen(state: NetworkSelectionUM, onDismiss: () -> Unit) {
+ Dialog(
+ onDismissRequest = onDismiss,
+ properties = DialogProperties(
+ usePlatformDefaultWidth = false,
+ decorFitsSystemWindows = false,
+ ),
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(TangemTheme.colors.background.tertiary)
+ .systemBarsPadding()
+ .imePadding(),
+ ) {
+ TangemTopAppBar(
+ title = stringResourceSafe(R.string.common_token_send),
+ startButton = TopAppBarButtonUM.Icon(
+ iconRes = R.drawable.ic_close_24,
+ onClicked = onDismiss,
+ ),
+ )
+ SearchBar(
+ state = state.searchBar,
+ modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing22),
+ )
+ Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing8))
+ if (state.walletGroups.isEmpty() && state.searchBar.query.isNotBlank()) {
+ NetworkSelectionEmpty(modifier = Modifier.weight(1f))
+ } else {
+ NetworkSelectionContent(state = state)
+ }
+ }
+ }
+}
+
+@Composable
+private fun NetworkSelectionEmpty(modifier: Modifier = Modifier) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = TangemTheme.dimens.spacing16),
+ ) {
+ Text(
+ text = stringResourceSafe(R.string.common_no_results),
+ style = TangemTheme.typography.body2,
+ color = TangemTheme.colors.text.tertiary,
+ textAlign = TextAlign.Center,
+ )
+ }
+}
+
+@Composable
+private fun NetworkSelectionContent(state: NetworkSelectionUM) {
+ LazyColumn(
+ modifier = Modifier.fillMaxWidth(),
+ contentPadding = PaddingValues(bottom = TangemTheme.dimens.spacing12),
+ ) {
+ state.walletGroups.forEach { walletGroup ->
+ item(key = "wallet_header_${walletGroup.userWalletId}") {
+ WalletHeader(walletGroup = walletGroup)
+ }
+ walletGroup.accounts.forEach { accountGroup ->
+ accountGroupItems(
+ walletGroup = walletGroup,
+ accountGroup = accountGroup,
+ isBalanceHidden = state.isBalanceHidden,
+ )
+ }
+ }
+ }
+}
+
+private fun LazyListScope.accountGroupItems(
+ walletGroup: WalletGroupUM,
+ accountGroup: AccountGroupUM,
+ isBalanceHidden: Boolean,
+) {
+ val hasHiddenTokens = accountGroup.hiddenTokensCount > 0
+ val lastIndex = accountGroup.tokens.lastIndex.inc() + if (hasHiddenTokens) 1 else 0
+ item(key = "account_${walletGroup.userWalletId}_${accountGroup.accountName}") {
+ AnimatedVisibility(
+ visible = walletGroup.isExpanded,
+ enter = fadeIn() + expandVertically(),
+ exit = fadeOut() + shrinkVertically(),
+ ) {
+ AccountHeader(
+ accountGroup = accountGroup,
+ modifier = Modifier.roundedShapeItemDecoration(
+ currentIndex = 0,
+ lastIndex = lastIndex,
+ radius = TangemTheme.dimens.radius14,
+ backgroundColor = TangemTheme.colors.background.action,
+ ),
+ )
+ }
+ }
+ itemsIndexed(
+ items = accountGroup.tokens,
+ key = { _, token -> "token_${walletGroup.userWalletId}_${token.id}" },
+ ) { index, tokenState ->
+ val indexWithHeader = index.inc()
+ AnimatedVisibility(
+ visible = walletGroup.isExpanded,
+ enter = fadeIn() + expandVertically(),
+ exit = fadeOut() + shrinkVertically(),
+ ) {
+ TokenItem(
+ state = tokenState,
+ isBalanceHidden = isBalanceHidden,
+ reorderableTokenListState = null,
+ modifier = Modifier.roundedShapeItemDecoration(
+ currentIndex = indexWithHeader,
+ lastIndex = lastIndex,
+ radius = TangemTheme.dimens.radius14,
+ backgroundColor = TangemTheme.colors.background.action,
+ ),
+ )
+ }
+ }
+ if (hasHiddenTokens) {
+ item(
+ key = "hidden_${walletGroup.userWalletId}_${accountGroup.accountName}",
+ ) {
+ AnimatedVisibility(
+ visible = walletGroup.isExpanded,
+ enter = fadeIn() + expandVertically(),
+ exit = fadeOut() + shrinkVertically(),
+ ) {
+ HiddenTokensFooter(
+ count = accountGroup.hiddenTokensCount,
+ modifier = Modifier.roundedShapeItemDecoration(
+ currentIndex = lastIndex,
+ lastIndex = lastIndex,
+ radius = TangemTheme.dimens.radius14,
+ backgroundColor = TangemTheme.colors.background.action,
+ ),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun WalletHeader(walletGroup: WalletGroupUM) {
+ val chevronRotation by animateFloatAsState(
+ targetValue = if (walletGroup.isExpanded) CHEVRON_COLLAPSED_ROTATION else CHEVRON_EXPANDED_ROTATION,
+ label = "chevron_rotation",
+ )
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = walletGroup.onExpandToggle)
+ .padding(
+ horizontal = TangemTheme.dimens.spacing16,
+ vertical = TangemTheme.dimens.spacing12,
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = walletGroup.walletName,
+ style = TangemTheme.typography.subtitle1,
+ color = TangemTheme.colors.text.primary1,
+ modifier = Modifier.weight(1f),
+ )
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .size(TangemTheme.dimens.size24)
+ .background(
+ color = TangemTheme.colors.button.secondary,
+ shape = CircleShape,
+ ),
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_chevron_up_24),
+ contentDescription = null,
+ tint = TangemTheme.colors.icon.informative,
+ modifier = Modifier
+ .size(TangemTheme.dimens.size16)
+ .rotate(chevronRotation),
+ )
+ }
+ }
+}
+
+@Composable
+private fun AccountHeader(accountGroup: AccountGroupUM, modifier: Modifier = Modifier) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(
+ start = TangemTheme.dimens.spacing12,
+ top = TangemTheme.dimens.spacing10,
+ bottom = TangemTheme.dimens.spacing6,
+ end = TangemTheme.dimens.spacing12,
+ ),
+ ) {
+ accountGroup.iconState?.let { iconState ->
+ CurrencyIcon(
+ state = iconState,
+ modifier = Modifier.size(TangemTheme.dimens.size18),
+ )
+ Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing6))
+ }
+ Text(
+ text = accountGroup.accountName.resolveReference(),
+ style = TangemTheme.typography.caption1,
+ color = TangemTheme.colors.text.tertiary,
+ )
+ }
+}
+
+@Composable
+private fun HiddenTokensFooter(count: Int, modifier: Modifier = Modifier) {
+ Column(modifier = modifier) {
+ HorizontalDivider(
+ thickness = 0.5.dp,
+ color = TangemTheme.colors.stroke.primary,
+ modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing14),
+ )
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ start = TangemTheme.dimens.spacing12,
+ end = TangemTheme.dimens.spacing12,
+ top = TangemTheme.dimens.spacing10,
+ bottom = TangemTheme.dimens.spacing10,
+ ),
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_eye_off_outline_24),
+ contentDescription = null,
+ tint = TangemTheme.colors.icon.informative,
+ modifier = Modifier.size(TangemTheme.dimens.size16),
+ )
+ Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing8))
+ Text(
+ text = pluralStringResourceSafe(
+ id = R.plurals.send_network_selection_hidden_tokens,
+ count = count,
+ count,
+ ),
+ style = TangemTheme.typography.caption2,
+ color = TangemTheme.colors.text.tertiary,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt
index db07b4492e..b2cf810c08 100644
--- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt
+++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt
@@ -281,9 +281,11 @@ internal class SendModel @Inject constructor(
}
is PredefinedValues.Content.QrCode -> {
val predefinedAmount = predefinedValues.amount?.parseBigDecimalOrNull()
+ val amount = predefinedAmount
+ ?: (uiState.value.amountUM as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
+ ?: error("Invalid amount")
createTransferTransactionUseCase(
- amount = predefinedAmount?.convertToSdkAmount(cryptoCurrencyStatus)
- ?: error("Invalid amount"),
+ amount = amount.convertToSdkAmount(cryptoCurrencyStatus),
memo = predefinedValues.memo,
destination = predefinedValues.address,
userWalletId = userWallet.walletId,
@@ -422,13 +424,24 @@ internal class SendModel @Inject constructor(
feeCryptoCurrencyStatusFlow,
) { cryptoCurrencyStatus, feeCryptoCurrencyStatus ->
if (isAvailableForSend && currentRoute.value == initialRoute) {
- router.replaceAll(Confirm)
+ if (isPredefinedAmountExceedsBalance(cryptoCurrencyStatus)) {
+ router.replaceAll(Amount(isEditMode = false))
+ } else {
+ router.replaceAll(Confirm)
+ }
} else if (isUnavailableForSend) {
showAlertError()
}
}.launchIn(modelScope)
}
+ private fun isPredefinedAmountExceedsBalance(cryptoCurrencyStatus: CryptoCurrencyStatus): Boolean {
+ val predefinedAmount = (predefinedValues as? PredefinedValues.Content)?.amount
+ ?.parseBigDecimalOrNull() ?: return false
+ val balance = cryptoCurrencyStatus.value.amount ?: return false
+ return predefinedAmount > balance
+ }
+
private fun CryptoCurrencyStatus.hasAvailableStatus(): Boolean = this.value is CryptoCurrencyStatus.Loaded ||
this.value is CryptoCurrencyStatus.Custom ||
this.value is CryptoCurrencyStatus.NoQuote