Updated on 2026-08-14
This commit is contained in:
parent
1470d4b08d
commit
8beb80f5a7
13 changed files with 778 additions and 34 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.tap.di.domain
|
package com.tangem.tap.di.domain
|
||||||
|
|
||||||
import com.tangem.domain.account.supplier.MultiAccountListSupplier
|
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.repository.QrScanningEventsRepository
|
||||||
import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase
|
import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase
|
||||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||||
|
|
@ -39,10 +40,12 @@ internal object QrScanningDomainModule {
|
||||||
fun provideResolveQrSendTargetsUseCase(
|
fun provideResolveQrSendTargetsUseCase(
|
||||||
multiAccountListSupplier: MultiAccountListSupplier,
|
multiAccountListSupplier: MultiAccountListSupplier,
|
||||||
qrScanningEventsRepository: QrScanningEventsRepository,
|
qrScanningEventsRepository: QrScanningEventsRepository,
|
||||||
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
): ResolveQrSendTargetsUseCase {
|
): ResolveQrSendTargetsUseCase {
|
||||||
return ResolveQrSendTargetsUseCase(
|
return ResolveQrSendTargetsUseCase(
|
||||||
multiAccountListSupplier = multiAccountListSupplier,
|
multiAccountListSupplier = multiAccountListSupplier,
|
||||||
qrScanningEventsRepository = qrScanningEventsRepository,
|
qrScanningEventsRepository = qrScanningEventsRepository,
|
||||||
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -217,6 +217,7 @@
|
||||||
<string name="common_add">Add</string>
|
<string name="common_add">Add</string>
|
||||||
<string name="common_add_to_portfolio">Add to portfolio</string>
|
<string name="common_add_to_portfolio">Add to portfolio</string>
|
||||||
<string name="common_add_token">Add token</string>
|
<string name="common_add_token">Add token</string>
|
||||||
|
<string name="common_add_tokens">Add tokens</string>
|
||||||
<string name="common_added">Added</string>
|
<string name="common_added">Added</string>
|
||||||
<string name="common_address">Address</string>
|
<string name="common_address">Address</string>
|
||||||
<string name="common_all">All</string>
|
<string name="common_all">All</string>
|
||||||
|
|
@ -325,6 +326,7 @@
|
||||||
<string name="common_nft">NFT</string>
|
<string name="common_nft">NFT</string>
|
||||||
<string name="common_no">No</string>
|
<string name="common_no">No</string>
|
||||||
<string name="common_no_address">No address</string>
|
<string name="common_no_address">No address</string>
|
||||||
|
<string name="common_no_results">No results</string>
|
||||||
<string name="common_not_added">Not Added</string>
|
<string name="common_not_added">Not Added</string>
|
||||||
<string name="common_not_available">Not available</string>
|
<string name="common_not_available">Not available</string>
|
||||||
<string name="common_not_now">Not now</string>
|
<string name="common_not_now">Not now</string>
|
||||||
|
|
@ -383,6 +385,7 @@
|
||||||
<string name="common_to">To</string>
|
<string name="common_to">To</string>
|
||||||
<string name="common_to_wallet_name">To %s</string>
|
<string name="common_to_wallet_name">To %s</string>
|
||||||
<string name="common_today">Today</string>
|
<string name="common_today">Today</string>
|
||||||
|
<string name="common_token_send">Token to send</string>
|
||||||
<plurals name="common_tokens_count">
|
<plurals name="common_tokens_count">
|
||||||
<item quantity="one">%d token</item>
|
<item quantity="one">%d token</item>
|
||||||
<item quantity="other">%d tokens</item>
|
<item quantity="other">%d tokens</item>
|
||||||
|
|
@ -395,6 +398,7 @@
|
||||||
<string name="common_understand">I understand</string>
|
<string name="common_understand">I understand</string>
|
||||||
<string name="common_understand_continue">I understand, continue</string>
|
<string name="common_understand_continue">I understand, continue</string>
|
||||||
<string name="common_unknown_error">There was an error. Please try again.</string>
|
<string name="common_unknown_error">There was an error. Please try again.</string>
|
||||||
|
<string name="common_unlock">Unlock</string>
|
||||||
<string name="common_unreachable">Unreachable</string>
|
<string name="common_unreachable">Unreachable</string>
|
||||||
<string name="common_unstake">Unstake</string>
|
<string name="common_unstake">Unstake</string>
|
||||||
<string name="common_utxo_validate_withdrawal_message_warning">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.</string>
|
<string name="common_utxo_validate_withdrawal_message_warning">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.</string>
|
||||||
|
|
@ -1211,6 +1215,10 @@
|
||||||
<string name="send_memo">Memo: %s</string>
|
<string name="send_memo">Memo: %s</string>
|
||||||
<string name="send_memo_destination_tag_error">Invalid Memo</string>
|
<string name="send_memo_destination_tag_error">Invalid Memo</string>
|
||||||
<string name="send_network_fee_warning_title">Network fee coverage</string>
|
<string name="send_network_fee_warning_title">Network fee coverage</string>
|
||||||
|
<plurals name="send_network_selection_hidden_tokens">
|
||||||
|
<item quantity="one">%d token isn\'t compatible with this address</item>
|
||||||
|
<item quantity="other">%d tokens aren\'t compatible with this address</item>
|
||||||
|
</plurals>
|
||||||
<string name="send_nonce">Nonce</string>
|
<string name="send_nonce">Nonce</string>
|
||||||
<string name="send_nonce_footer">Unique number for each transaction. Use it to resend or cancel a pending transaction.</string>
|
<string name="send_nonce_footer">Unique number for each transaction. Use it to resend or cancel a pending transaction.</string>
|
||||||
<string name="send_nonce_hint">Enter nonce…</string>
|
<string name="send_nonce_hint">Enter nonce…</string>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ dependencies {
|
||||||
/** Domain */
|
/** Domain */
|
||||||
api(projects.domain.models)
|
api(projects.domain.models)
|
||||||
implementation(projects.domain.account)
|
implementation(projects.domain.account)
|
||||||
|
implementation(projects.domain.common)
|
||||||
implementation(projects.domain.qrScanning.models)
|
implementation(projects.domain.qrScanning.models)
|
||||||
implementation(projects.domain.tokens.models)
|
implementation(projects.domain.tokens.models)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ sealed class QrSendTarget {
|
||||||
val accountId: AccountId,
|
val accountId: AccountId,
|
||||||
val accountName: AccountName,
|
val accountName: AccountName,
|
||||||
val currencies: List<CryptoCurrency>,
|
val currencies: List<CryptoCurrency>,
|
||||||
|
val hiddenTokensCount: Int = 0,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import com.tangem.domain.models.account.AccountId
|
||||||
import com.tangem.domain.models.account.AccountName
|
import com.tangem.domain.models.account.AccountName
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
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.models.ClassifiedQrContent
|
||||||
import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
|
import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
@ -14,41 +15,41 @@ import com.tangem.domain.qrscanning.models.QrSendTarget
|
||||||
class ResolveQrSendTargetsUseCase(
|
class ResolveQrSendTargetsUseCase(
|
||||||
private val multiAccountListSupplier: MultiAccountListSupplier,
|
private val multiAccountListSupplier: MultiAccountListSupplier,
|
||||||
private val qrScanningEventsRepository: QrScanningEventsRepository,
|
private val qrScanningEventsRepository: QrScanningEventsRepository,
|
||||||
|
private val userWalletsListRepository: UserWalletsListRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend operator fun invoke(qrCode: String): QrSendTarget {
|
suspend operator fun invoke(qrCode: String): QrSendTarget {
|
||||||
val allAccountLists = multiAccountListSupplier.getSyncOrNull(Unit).orEmpty()
|
val allAccountLists = multiAccountListSupplier.getSyncOrNull(Unit).orEmpty()
|
||||||
|
val userWallets = userWalletsListRepository.userWalletsSync()
|
||||||
|
val walletNamesMap = userWallets.associate { it.walletId to it.name }
|
||||||
|
|
||||||
val currencyEntries = allAccountLists.flatMap { accountList ->
|
val allCurrencies = mutableListOf<CryptoCurrency>()
|
||||||
accountList.accounts
|
val currencyLocations = mutableMapOf<CryptoCurrency.ID, MutableList<CurrencyLocation>>()
|
||||||
.filterIsInstance<Account.CryptoPortfolio>()
|
val totalPerAccount = mutableMapOf<AccountId, Int>()
|
||||||
.flatMap { account ->
|
|
||||||
account.cryptoCurrencies.map { currency ->
|
for (accountList in allAccountLists) {
|
||||||
currency to CurrencyLocation(
|
for (account in accountList.accounts.filterIsInstance<Account.CryptoPortfolio>()) {
|
||||||
userWalletId = accountList.userWalletId,
|
val location = CurrencyLocation(
|
||||||
walletName = accountList.userWalletId.stringValue,
|
walletName = walletNamesMap[account.accountId.userWalletId]
|
||||||
accountId = account.accountId,
|
?: account.accountId.userWalletId.stringValue,
|
||||||
accountName = account.accountName,
|
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 classified = qrScanningEventsRepository.classify(qrCode, allCurrencies)
|
||||||
|
val portfolioIndex = PortfolioIndex(currencyLocations, totalPerAccount)
|
||||||
|
|
||||||
return resolve(classified, currencyLocations)
|
return resolve(classified, portfolioIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolve(
|
private fun resolve(classified: ClassifiedQrContent, portfolioIndex: PortfolioIndex): QrSendTarget {
|
||||||
classified: ClassifiedQrContent,
|
|
||||||
currencyLocations: Map<CryptoCurrency.ID, List<CurrencyLocation>>,
|
|
||||||
): QrSendTarget {
|
|
||||||
return when (classified) {
|
return when (classified) {
|
||||||
is ClassifiedQrContent.WalletConnect -> QrSendTarget.WalletConnect(classified.uri)
|
is ClassifiedQrContent.WalletConnect -> QrSendTarget.WalletConnect(classified.uri)
|
||||||
is ClassifiedQrContent.Unknown -> QrSendTarget.Unknown(classified.raw)
|
is ClassifiedQrContent.Unknown -> QrSendTarget.Unknown(classified.raw)
|
||||||
|
|
@ -57,14 +58,14 @@ class ResolveQrSendTargetsUseCase(
|
||||||
amount = null,
|
amount = null,
|
||||||
memo = null,
|
memo = null,
|
||||||
matchingCurrencies = classified.matchingCurrencies,
|
matchingCurrencies = classified.matchingCurrencies,
|
||||||
currencyLocations = currencyLocations,
|
portfolioIndex = portfolioIndex,
|
||||||
)
|
)
|
||||||
is ClassifiedQrContent.PaymentUri -> resolveAddressTarget(
|
is ClassifiedQrContent.PaymentUri -> resolveAddressTarget(
|
||||||
address = classified.address,
|
address = classified.address,
|
||||||
amount = classified.amount,
|
amount = classified.amount,
|
||||||
memo = classified.memo,
|
memo = classified.memo,
|
||||||
matchingCurrencies = classified.matchingCurrencies,
|
matchingCurrencies = classified.matchingCurrencies,
|
||||||
currencyLocations = currencyLocations,
|
portfolioIndex = portfolioIndex,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -74,9 +75,9 @@ class ResolveQrSendTargetsUseCase(
|
||||||
amount: BigDecimal?,
|
amount: BigDecimal?,
|
||||||
memo: String?,
|
memo: String?,
|
||||||
matchingCurrencies: List<CryptoCurrency>,
|
matchingCurrencies: List<CryptoCurrency>,
|
||||||
currencyLocations: Map<CryptoCurrency.ID, List<CurrencyLocation>>,
|
portfolioIndex: PortfolioIndex,
|
||||||
): QrSendTarget {
|
): QrSendTarget {
|
||||||
val walletGroups = buildWalletGroups(matchingCurrencies, currencyLocations)
|
val walletGroups = buildWalletGroups(matchingCurrencies, portfolioIndex)
|
||||||
|
|
||||||
val singleGroup = walletGroups.singleOrNull()
|
val singleGroup = walletGroups.singleOrNull()
|
||||||
val singleCurrency = singleGroup?.accounts?.singleOrNull()?.currencies?.singleOrNull()
|
val singleCurrency = singleGroup?.accounts?.singleOrNull()?.currencies?.singleOrNull()
|
||||||
|
|
@ -101,15 +102,15 @@ class ResolveQrSendTargetsUseCase(
|
||||||
|
|
||||||
private fun buildWalletGroups(
|
private fun buildWalletGroups(
|
||||||
matchingCurrencies: List<CryptoCurrency>,
|
matchingCurrencies: List<CryptoCurrency>,
|
||||||
currencyLocations: Map<CryptoCurrency.ID, List<CurrencyLocation>>,
|
portfolioIndex: PortfolioIndex,
|
||||||
): List<QrSendTarget.Multiple.WalletGroup> {
|
): List<QrSendTarget.Multiple.WalletGroup> {
|
||||||
val walletMap = linkedMapOf<UserWalletId, WalletInfo>()
|
val walletMap = linkedMapOf<UserWalletId, WalletInfo>()
|
||||||
val uniqueCurrencies = matchingCurrencies.distinctBy { it.id }
|
val uniqueCurrencies = matchingCurrencies.distinctBy { it.id }
|
||||||
|
|
||||||
for (currency in uniqueCurrencies) {
|
for (currency in uniqueCurrencies) {
|
||||||
val locations = currencyLocations[currency.id] ?: continue
|
val locations = portfolioIndex.currencyLocations[currency.id].orEmpty()
|
||||||
for (location in locations) {
|
for (location in locations) {
|
||||||
val walletInfo = walletMap.getOrPut(location.userWalletId) {
|
val walletInfo = walletMap.getOrPut(location.accountId.userWalletId) {
|
||||||
WalletInfo(location.walletName, linkedMapOf())
|
WalletInfo(location.walletName, linkedMapOf())
|
||||||
}
|
}
|
||||||
val accountInfo = walletInfo.accounts.getOrPut(location.accountId) {
|
val accountInfo = walletInfo.accounts.getOrPut(location.accountId) {
|
||||||
|
|
@ -124,18 +125,24 @@ class ResolveQrSendTargetsUseCase(
|
||||||
userWalletId = walletId,
|
userWalletId = walletId,
|
||||||
walletName = walletInfo.walletName,
|
walletName = walletInfo.walletName,
|
||||||
accounts = walletInfo.accounts.map { (accountId, accountInfo) ->
|
accounts = walletInfo.accounts.map { (accountId, accountInfo) ->
|
||||||
|
val total = portfolioIndex.totalPerAccount[accountId] ?: 0
|
||||||
QrSendTarget.Multiple.AccountGroup(
|
QrSendTarget.Multiple.AccountGroup(
|
||||||
accountId = accountId,
|
accountId = accountId,
|
||||||
accountName = accountInfo.accountName,
|
accountName = accountInfo.accountName,
|
||||||
currencies = accountInfo.currencies,
|
currencies = accountInfo.currencies,
|
||||||
|
hiddenTokensCount = total - accountInfo.currencies.size,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class PortfolioIndex(
|
||||||
|
val currencyLocations: Map<CryptoCurrency.ID, List<CurrencyLocation>>,
|
||||||
|
val totalPerAccount: Map<AccountId, Int>,
|
||||||
|
)
|
||||||
|
|
||||||
private data class CurrencyLocation(
|
private data class CurrencyLocation(
|
||||||
val userWalletId: UserWalletId,
|
|
||||||
val walletName: String,
|
val walletName: String,
|
||||||
val accountId: AccountId,
|
val accountId: AccountId,
|
||||||
val accountName: AccountName,
|
val accountName: AccountName,
|
||||||
|
|
|
||||||
|
|
@ -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<WalletGroup>,
|
||||||
|
val onTokenSelected: (UserWalletId, CryptoCurrency) -> Unit,
|
||||||
|
val onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
data class WalletGroup(
|
||||||
|
val userWalletId: UserWalletId,
|
||||||
|
val walletName: String,
|
||||||
|
val accounts: List<AccountGroup>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AccountGroup(
|
||||||
|
val accountId: AccountId,
|
||||||
|
val accountName: AccountName,
|
||||||
|
val currencies: List<CryptoCurrency>,
|
||||||
|
val hiddenTokensCount: Int = 0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Factory : ComponentFactory<Params, NetworkSelectionComponent>
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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<WalletGroupUM>,
|
||||||
|
val isBalanceHidden: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
internal data class WalletGroupUM(
|
||||||
|
val userWalletId: UserWalletId,
|
||||||
|
val walletName: String,
|
||||||
|
val isExpanded: Boolean,
|
||||||
|
val onExpandToggle: () -> Unit,
|
||||||
|
val accounts: ImmutableList<AccountGroupUM>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
internal data class AccountGroupUM(
|
||||||
|
val accountName: TextReference,
|
||||||
|
val iconState: CurrencyIconState.CryptoPortfolio?,
|
||||||
|
val tokens: ImmutableList<TokenItemState>,
|
||||||
|
val hiddenTokensCount: Int,
|
||||||
|
)
|
||||||
|
|
@ -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<NetworkSelectionUM> = createUiStateFlow()
|
||||||
|
|
||||||
|
private fun createUiStateFlow(): StateFlow<NetworkSelectionUM> {
|
||||||
|
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<UserWalletId>,
|
||||||
|
appCurrency: AppCurrency,
|
||||||
|
statusLists: List<AccountStatusList>,
|
||||||
|
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<UserWalletId>,
|
||||||
|
appCurrency: AppCurrency,
|
||||||
|
statusLists: List<AccountStatusList>,
|
||||||
|
): 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<CryptoCurrency.ID, CryptoCurrencyStatus> {
|
||||||
|
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<CryptoCurrency.ID, CryptoCurrencyStatus>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -281,9 +281,11 @@ internal class SendModel @Inject constructor(
|
||||||
}
|
}
|
||||||
is PredefinedValues.Content.QrCode -> {
|
is PredefinedValues.Content.QrCode -> {
|
||||||
val predefinedAmount = predefinedValues.amount?.parseBigDecimalOrNull()
|
val predefinedAmount = predefinedValues.amount?.parseBigDecimalOrNull()
|
||||||
|
val amount = predefinedAmount
|
||||||
|
?: (uiState.value.amountUM as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
|
||||||
|
?: error("Invalid amount")
|
||||||
createTransferTransactionUseCase(
|
createTransferTransactionUseCase(
|
||||||
amount = predefinedAmount?.convertToSdkAmount(cryptoCurrencyStatus)
|
amount = amount.convertToSdkAmount(cryptoCurrencyStatus),
|
||||||
?: error("Invalid amount"),
|
|
||||||
memo = predefinedValues.memo,
|
memo = predefinedValues.memo,
|
||||||
destination = predefinedValues.address,
|
destination = predefinedValues.address,
|
||||||
userWalletId = userWallet.walletId,
|
userWalletId = userWallet.walletId,
|
||||||
|
|
@ -422,13 +424,24 @@ internal class SendModel @Inject constructor(
|
||||||
feeCryptoCurrencyStatusFlow,
|
feeCryptoCurrencyStatusFlow,
|
||||||
) { cryptoCurrencyStatus, feeCryptoCurrencyStatus ->
|
) { cryptoCurrencyStatus, feeCryptoCurrencyStatus ->
|
||||||
if (isAvailableForSend && currentRoute.value == initialRoute) {
|
if (isAvailableForSend && currentRoute.value == initialRoute) {
|
||||||
router.replaceAll(Confirm)
|
if (isPredefinedAmountExceedsBalance(cryptoCurrencyStatus)) {
|
||||||
|
router.replaceAll(Amount(isEditMode = false))
|
||||||
|
} else {
|
||||||
|
router.replaceAll(Confirm)
|
||||||
|
}
|
||||||
} else if (isUnavailableForSend) {
|
} else if (isUnavailableForSend) {
|
||||||
showAlertError()
|
showAlertError()
|
||||||
}
|
}
|
||||||
}.launchIn(modelScope)
|
}.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 ||
|
private fun CryptoCurrencyStatus.hasAvailableStatus(): Boolean = this.value is CryptoCurrencyStatus.Loaded ||
|
||||||
this.value is CryptoCurrencyStatus.Custom ||
|
this.value is CryptoCurrencyStatus.Custom ||
|
||||||
this.value is CryptoCurrencyStatus.NoQuote
|
this.value is CryptoCurrencyStatus.NoQuote
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue