Updated on 2026-08-14
This commit is contained in:
parent
5e6bf6aaae
commit
aaf4419ebe
12 changed files with 257 additions and 58 deletions
|
|
@ -14,6 +14,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||||
import com.tangem.datasource.local.preferences.utils.getObjectList
|
import com.tangem.datasource.local.preferences.utils.getObjectList
|
||||||
import com.tangem.datasource.local.preferences.utils.getObjectListSync
|
import com.tangem.datasource.local.preferences.utils.getObjectListSync
|
||||||
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
||||||
|
import com.tangem.domain.models.account.AccountId
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
|
@ -54,7 +55,7 @@ internal class DefaultSwapTransactionRepository(
|
||||||
storeTransactionState(
|
storeTransactionState(
|
||||||
txId = transaction.txId,
|
txId = transaction.txId,
|
||||||
status = it,
|
status = it,
|
||||||
refundTokenCurrency = null,
|
accountWithCurrency = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
appPreferencesStore.editData { mutablePreferences ->
|
appPreferencesStore.editData { mutablePreferences ->
|
||||||
|
|
@ -177,7 +178,7 @@ internal class DefaultSwapTransactionRepository(
|
||||||
override suspend fun storeTransactionState(
|
override suspend fun storeTransactionState(
|
||||||
txId: String,
|
txId: String,
|
||||||
status: SwapStatusModel,
|
status: SwapStatusModel,
|
||||||
refundTokenCurrency: CryptoCurrency?,
|
accountWithCurrency: Pair<AccountId?, CryptoCurrency>?,
|
||||||
) {
|
) {
|
||||||
appPreferencesStore.editData { mutablePreferences ->
|
appPreferencesStore.editData { mutablePreferences ->
|
||||||
val savedMap = mutablePreferences.getObjectMap<SwapStatusDTO>(
|
val savedMap = mutablePreferences.getObjectMap<SwapStatusDTO>(
|
||||||
|
|
@ -187,8 +188,8 @@ internal class DefaultSwapTransactionRepository(
|
||||||
val updatesMap = savedMap.toMutableMap()
|
val updatesMap = savedMap.toMutableMap()
|
||||||
updatesMap[txId] = savedStatusConverter.convert(
|
updatesMap[txId] = savedStatusConverter.convert(
|
||||||
status.copy(
|
status.copy(
|
||||||
refundTokensResponse = refundTokenCurrency?.let {
|
refundTokensResponse = accountWithCurrency?.let { (accountId, currency) ->
|
||||||
userTokensResponseFactory.createResponseToken(refundTokenCurrency)
|
userTokensResponseFactory.createResponseToken(currency, accountId)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,12 @@ internal class SavedSwapTransactionListConverter(
|
||||||
fromCryptoCurrencyId = value.fromCryptoCurrencyId,
|
fromCryptoCurrencyId = value.fromCryptoCurrencyId,
|
||||||
toCryptoCurrencyId = value.toCryptoCurrencyId,
|
toCryptoCurrencyId = value.toCryptoCurrencyId,
|
||||||
fromTokensResponse = userTokensResponseFactory.createResponseToken(
|
fromTokensResponse = userTokensResponseFactory.createResponseToken(
|
||||||
value.fromCryptoCurrency,
|
currency = value.fromCryptoCurrency,
|
||||||
|
accountId = null,
|
||||||
),
|
),
|
||||||
toTokensResponse = userTokensResponseFactory.createResponseToken(
|
toTokensResponse = userTokensResponseFactory.createResponseToken(
|
||||||
value.toCryptoCurrency,
|
currency = value.toCryptoCurrency,
|
||||||
|
accountId = null,
|
||||||
),
|
),
|
||||||
transactions = savedSwapTransactionConverter.convertList(value.transactions),
|
transactions = savedSwapTransactionConverter.convertList(value.transactions),
|
||||||
)
|
)
|
||||||
|
|
@ -74,8 +76,11 @@ internal class SavedSwapTransactionListConverter(
|
||||||
userWalletId = userWalletId.stringValue,
|
userWalletId = userWalletId.stringValue,
|
||||||
fromCryptoCurrencyId = fromCryptoCurrency.id.value,
|
fromCryptoCurrencyId = fromCryptoCurrency.id.value,
|
||||||
toCryptoCurrencyId = toCryptoCurrency.id.value,
|
toCryptoCurrencyId = toCryptoCurrency.id.value,
|
||||||
fromTokensResponse = userTokensResponseFactory.createResponseToken(fromCryptoCurrency),
|
fromTokensResponse = userTokensResponseFactory.createResponseToken(
|
||||||
toTokensResponse = userTokensResponseFactory.createResponseToken(toCryptoCurrency),
|
currency = fromCryptoCurrency,
|
||||||
|
accountId = null,
|
||||||
|
),
|
||||||
|
toTokensResponse = userTokensResponseFactory.createResponseToken(currency = toCryptoCurrency, accountId = null),
|
||||||
transactions = tokenTransactions,
|
transactions = tokenTransactions,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -57,8 +57,16 @@ class SaveCryptoCurrenciesUseCase(
|
||||||
|
|
||||||
suspend operator fun invoke(
|
suspend operator fun invoke(
|
||||||
accountId: AccountId,
|
accountId: AccountId,
|
||||||
add: List<CryptoCurrency>,
|
add: CryptoCurrency? = null,
|
||||||
remove: List<CryptoCurrency>,
|
remove: CryptoCurrency? = null,
|
||||||
|
): Either<Throwable, Unit> {
|
||||||
|
return invoke(accountId = accountId, add = listOfNotNull(add), remove = listOfNotNull(remove))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend operator fun invoke(
|
||||||
|
accountId: AccountId,
|
||||||
|
add: List<CryptoCurrency> = emptyList(),
|
||||||
|
remove: List<CryptoCurrency> = emptyList(),
|
||||||
): Either<Throwable, Unit> = eitherOn(dispatchers.default) {
|
): Either<Throwable, Unit> = eitherOn(dispatchers.default) {
|
||||||
if (add.isEmpty() && remove.isEmpty()) {
|
if (add.isEmpty() && remove.isEmpty()) {
|
||||||
Timber.d("No currencies to add or remove, skipping")
|
Timber.d("No currencies to add or remove, skipping")
|
||||||
|
|
@ -84,6 +92,34 @@ class SaveCryptoCurrenciesUseCase(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun add(
|
||||||
|
accountId: AccountId,
|
||||||
|
networkId: String,
|
||||||
|
contractAddress: String,
|
||||||
|
): Either<Throwable, CryptoCurrency> = eitherOn(dispatchers.default) {
|
||||||
|
val userWalletId = accountId.userWalletId
|
||||||
|
|
||||||
|
withContext(NonCancellable) {
|
||||||
|
val account = getAccount(accountId = accountId)
|
||||||
|
|
||||||
|
val foundToken = account.cryptoCurrencies
|
||||||
|
.filterIsInstance<CryptoCurrency.Token>()
|
||||||
|
.firstOrNull {
|
||||||
|
it.network.backendId == networkId &&
|
||||||
|
!it.isCustom &&
|
||||||
|
it.contractAddress.equals(contractAddress, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundToken != null) return@withContext foundToken
|
||||||
|
|
||||||
|
val tokenToAdd = findToken(userWalletId, contractAddress, networkId)
|
||||||
|
|
||||||
|
refreshBalances(userWalletId = userWalletId, currencies = listOf(tokenToAdd)).joinAll()
|
||||||
|
|
||||||
|
tokenToAdd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun Raise<Throwable>.getAccount(accountId: AccountId): Account.CryptoPortfolio {
|
private suspend fun Raise<Throwable>.getAccount(accountId: AccountId): Account.CryptoPortfolio {
|
||||||
val accountList = singleAccountListSupplier.getSyncOrNull(
|
val accountList = singleAccountListSupplier.getSyncOrNull(
|
||||||
params = SingleAccountListProducer.Params(userWalletId = accountId.userWalletId),
|
params = SingleAccountListProducer.Params(userWalletId = accountId.userWalletId),
|
||||||
|
|
@ -181,6 +217,23 @@ class SaveCryptoCurrenciesUseCase(
|
||||||
return destination
|
return destination
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun Raise<Throwable>.findToken(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
networkId: String,
|
||||||
|
contractAddress: String,
|
||||||
|
): CryptoCurrency.Token {
|
||||||
|
return catch(
|
||||||
|
block = {
|
||||||
|
currenciesRepository.createTokenCurrency(
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
networkId = networkId,
|
||||||
|
contractAddress = contractAddress,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
catch = ::raise,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun refreshBalances(userWalletId: UserWalletId, currencies: List<CryptoCurrency>): List<Job> {
|
private suspend fun refreshBalances(userWalletId: UserWalletId, currencies: List<CryptoCurrency>): List<Job> {
|
||||||
if (currencies.isEmpty()) return emptyList()
|
if (currencies.isEmpty()) return emptyList()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.tangem.domain.swap
|
package com.tangem.domain.swap
|
||||||
|
|
||||||
|
import com.tangem.domain.models.account.AccountId
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
|
@ -59,9 +60,13 @@ interface SwapTransactionRepository {
|
||||||
*
|
*
|
||||||
* @param txId transaction id to update
|
* @param txId transaction id to update
|
||||||
* @param status new transaction status
|
* @param status new transaction status
|
||||||
* @param refundTokenCurrency refund token
|
* @param accountWithCurrency account id with refund token
|
||||||
*/
|
*/
|
||||||
suspend fun storeTransactionState(txId: String, status: SwapStatusModel, refundTokenCurrency: CryptoCurrency?)
|
suspend fun storeTransactionState(
|
||||||
|
txId: String,
|
||||||
|
status: SwapStatusModel,
|
||||||
|
accountWithCurrency: Pair<AccountId?, CryptoCurrency>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save last swapped crypto currency token
|
* Save last swapped crypto currency token
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,14 @@ import com.tangem.domain.tokens.model.remove.RemoveCurrencyError
|
||||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
|
|
||||||
|
@Deprecated("Use SaveCryptoCurrenciesUseCase")
|
||||||
class RemoveCurrencyUseCase(
|
class RemoveCurrencyUseCase(
|
||||||
private val currenciesRepository: CurrenciesRepository,
|
private val currenciesRepository: CurrenciesRepository,
|
||||||
private val walletManagersFacade: WalletManagersFacade,
|
private val walletManagersFacade: WalletManagersFacade,
|
||||||
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@Deprecated("Use SaveCryptoCurrenciesUseCase")
|
||||||
suspend operator fun invoke(
|
suspend operator fun invoke(
|
||||||
userWalletId: UserWalletId,
|
userWalletId: UserWalletId,
|
||||||
currency: CryptoCurrency,
|
currency: CryptoCurrency,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||||
import com.tangem.datasource.local.preferences.utils.getObjectList
|
import com.tangem.datasource.local.preferences.utils.getObjectList
|
||||||
import com.tangem.datasource.local.preferences.utils.getObjectListSync
|
import com.tangem.datasource.local.preferences.utils.getObjectListSync
|
||||||
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
||||||
|
import com.tangem.domain.models.account.AccountId
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
|
@ -40,7 +41,7 @@ internal class DefaultSwapTransactionRepository(
|
||||||
storeTransactionState(
|
storeTransactionState(
|
||||||
txId = transaction.txId,
|
txId = transaction.txId,
|
||||||
status = it,
|
status = it,
|
||||||
refundTokenCurrency = null,
|
accountWithCurrency = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
appPreferencesStore.editData { mutablePreferences ->
|
appPreferencesStore.editData { mutablePreferences ->
|
||||||
|
|
@ -181,7 +182,7 @@ internal class DefaultSwapTransactionRepository(
|
||||||
override suspend fun storeTransactionState(
|
override suspend fun storeTransactionState(
|
||||||
txId: String,
|
txId: String,
|
||||||
status: ExchangeStatusModel,
|
status: ExchangeStatusModel,
|
||||||
refundTokenCurrency: CryptoCurrency?,
|
accountWithCurrency: Pair<AccountId?, CryptoCurrency>?,
|
||||||
) {
|
) {
|
||||||
appPreferencesStore.editData { mutablePreferences ->
|
appPreferencesStore.editData { mutablePreferences ->
|
||||||
val savedMap = mutablePreferences.getObjectMap<ExchangeStatusModel>(
|
val savedMap = mutablePreferences.getObjectMap<ExchangeStatusModel>(
|
||||||
|
|
@ -190,8 +191,8 @@ internal class DefaultSwapTransactionRepository(
|
||||||
|
|
||||||
val updatesMap = savedMap.toMutableMap()
|
val updatesMap = savedMap.toMutableMap()
|
||||||
updatesMap[txId] = status.copy(
|
updatesMap[txId] = status.copy(
|
||||||
refundTokensResponse = refundTokenCurrency?.let {
|
refundTokensResponse = accountWithCurrency?.let { (accountId, currency) ->
|
||||||
userTokensResponseFactory.createResponseToken(refundTokenCurrency)
|
userTokensResponseFactory.createResponseToken(currency, accountId)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,12 @@ internal class SavedSwapTransactionListConverter(
|
||||||
fromCryptoCurrencyId = value.fromCryptoCurrencyId,
|
fromCryptoCurrencyId = value.fromCryptoCurrencyId,
|
||||||
toCryptoCurrencyId = value.toCryptoCurrencyId,
|
toCryptoCurrencyId = value.toCryptoCurrencyId,
|
||||||
fromTokensResponse = userTokensResponseFactory.createResponseToken(
|
fromTokensResponse = userTokensResponseFactory.createResponseToken(
|
||||||
value.fromCryptoCurrency,
|
currency = value.fromCryptoCurrency,
|
||||||
|
accountId = null,
|
||||||
),
|
),
|
||||||
toTokensResponse = userTokensResponseFactory.createResponseToken(
|
toTokensResponse = userTokensResponseFactory.createResponseToken(
|
||||||
value.toCryptoCurrency,
|
currency = value.toCryptoCurrency,
|
||||||
|
accountId = null,
|
||||||
),
|
),
|
||||||
transactions = value.transactions,
|
transactions = value.transactions,
|
||||||
)
|
)
|
||||||
|
|
@ -82,8 +84,11 @@ internal class SavedSwapTransactionListConverter(
|
||||||
userWalletId = userWalletId.stringValue,
|
userWalletId = userWalletId.stringValue,
|
||||||
fromCryptoCurrencyId = fromCryptoCurrency.id.value,
|
fromCryptoCurrencyId = fromCryptoCurrency.id.value,
|
||||||
toCryptoCurrencyId = toCryptoCurrency.id.value,
|
toCryptoCurrencyId = toCryptoCurrency.id.value,
|
||||||
fromTokensResponse = userTokensResponseFactory.createResponseToken(fromCryptoCurrency),
|
fromTokensResponse = userTokensResponseFactory.createResponseToken(
|
||||||
toTokensResponse = userTokensResponseFactory.createResponseToken(toCryptoCurrency),
|
currency = fromCryptoCurrency,
|
||||||
|
accountId = null,
|
||||||
|
),
|
||||||
|
toTokensResponse = userTokensResponseFactory.createResponseToken(currency = toCryptoCurrency, accountId = null),
|
||||||
transactions = tokenTransactions,
|
transactions = tokenTransactions,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.tangem.feature.swap.domain
|
package com.tangem.feature.swap.domain
|
||||||
|
|
||||||
|
import com.tangem.domain.models.account.AccountId
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
|
@ -29,7 +30,11 @@ interface SwapTransactionRepository {
|
||||||
txId: String,
|
txId: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun storeTransactionState(txId: String, status: ExchangeStatusModel, refundTokenCurrency: CryptoCurrency?)
|
suspend fun storeTransactionState(
|
||||||
|
txId: String,
|
||||||
|
status: ExchangeStatusModel,
|
||||||
|
accountWithCurrency: Pair<AccountId?, CryptoCurrency>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun storeLastSwappedCryptoCurrencyId(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID)
|
suspend fun storeLastSwappedCryptoCurrencyId(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,35 +57,36 @@ dependencies {
|
||||||
implementation(projects.core.decompose)
|
implementation(projects.core.decompose)
|
||||||
implementation(projects.common.ui)
|
implementation(projects.common.ui)
|
||||||
|
|
||||||
|
implementation(projects.libs.blockchainSdk)
|
||||||
implementation(projects.libs.crypto)
|
implementation(projects.libs.crypto)
|
||||||
|
|
||||||
/** Domain modules */
|
/** Domain modules */
|
||||||
|
implementation(projects.domain.account.status)
|
||||||
implementation(projects.domain.appCurrency)
|
implementation(projects.domain.appCurrency)
|
||||||
implementation(projects.domain.appCurrency.models)
|
implementation(projects.domain.appCurrency.models)
|
||||||
|
implementation(projects.domain.balanceHiding)
|
||||||
|
implementation(projects.domain.balanceHiding.models)
|
||||||
implementation(projects.domain.card)
|
implementation(projects.domain.card)
|
||||||
implementation(projects.domain.demo)
|
implementation(projects.domain.demo)
|
||||||
implementation(projects.domain.legacy)
|
implementation(projects.domain.legacy)
|
||||||
implementation(projects.libs.blockchainSdk)
|
|
||||||
implementation(projects.domain.models)
|
|
||||||
implementation(projects.domain.settings)
|
|
||||||
implementation(projects.domain.tokens)
|
|
||||||
implementation(projects.domain.tokens.models)
|
|
||||||
implementation(projects.domain.txhistory)
|
|
||||||
implementation(projects.domain.txhistory.models)
|
|
||||||
implementation(projects.domain.wallets)
|
|
||||||
implementation(projects.domain.wallets.models)
|
|
||||||
implementation(projects.domain.balanceHiding)
|
|
||||||
implementation(projects.domain.balanceHiding.models)
|
|
||||||
implementation(projects.domain.transaction)
|
|
||||||
implementation(projects.domain.transaction.models)
|
|
||||||
implementation(projects.domain.staking)
|
|
||||||
implementation(projects.domain.markets.models)
|
implementation(projects.domain.markets.models)
|
||||||
|
implementation(projects.domain.models)
|
||||||
|
implementation(projects.domain.notifications.models)
|
||||||
implementation(projects.domain.onramp)
|
implementation(projects.domain.onramp)
|
||||||
implementation(projects.domain.onramp.models)
|
implementation(projects.domain.onramp.models)
|
||||||
implementation(projects.domain.promo)
|
implementation(projects.domain.promo)
|
||||||
implementation(projects.domain.promo.models)
|
implementation(projects.domain.promo.models)
|
||||||
implementation(projects.domain.quotes)
|
implementation(projects.domain.quotes)
|
||||||
implementation(projects.domain.notifications.models)
|
implementation(projects.domain.settings)
|
||||||
|
implementation(projects.domain.staking)
|
||||||
|
implementation(projects.domain.tokens)
|
||||||
|
implementation(projects.domain.tokens.models)
|
||||||
|
implementation(projects.domain.transaction)
|
||||||
|
implementation(projects.domain.transaction.models)
|
||||||
|
implementation(projects.domain.txhistory)
|
||||||
|
implementation(projects.domain.txhistory.models)
|
||||||
|
implementation(projects.domain.wallets)
|
||||||
|
implementation(projects.domain.wallets.models)
|
||||||
|
|
||||||
/** Temp dependency to swap domain */
|
/** Temp dependency to swap domain */
|
||||||
implementation(projects.features.swap.domain)
|
implementation(projects.features.swap.domain)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.model
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import arrow.core.getOrElse
|
import arrow.core.getOrElse
|
||||||
import arrow.core.merge
|
import arrow.core.merge
|
||||||
|
import arrow.core.right
|
||||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||||
import com.arkivanov.decompose.router.slot.activate
|
import com.arkivanov.decompose.router.slot.activate
|
||||||
import com.arkivanov.decompose.router.slot.dismiss
|
import com.arkivanov.decompose.router.slot.dismiss
|
||||||
|
|
@ -31,12 +32,16 @@ import com.tangem.core.ui.extensions.wrappedList
|
||||||
import com.tangem.core.ui.haptic.TangemHapticEffect
|
import com.tangem.core.ui.haptic.TangemHapticEffect
|
||||||
import com.tangem.core.ui.haptic.VibratorHapticManager
|
import com.tangem.core.ui.haptic.VibratorHapticManager
|
||||||
import com.tangem.core.ui.message.SnackbarMessage
|
import com.tangem.core.ui.message.SnackbarMessage
|
||||||
|
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||||
|
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||||
|
import com.tangem.domain.account.status.usecase.SaveCryptoCurrenciesUseCase
|
||||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||||
import com.tangem.domain.models.TokenReceiveNotification
|
import com.tangem.domain.models.TokenReceiveNotification
|
||||||
|
import com.tangem.domain.models.account.Account
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
import com.tangem.domain.models.network.NetworkAddress
|
import com.tangem.domain.models.network.NetworkAddress
|
||||||
|
|
@ -141,6 +146,9 @@ internal class TokenDetailsModel @Inject constructor(
|
||||||
private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase,
|
private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase,
|
||||||
private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase,
|
private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase,
|
||||||
private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase,
|
private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase,
|
||||||
|
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||||
|
private val getAccountCryptoCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||||
|
private val saveCryptoCurrenciesUseCase: SaveCryptoCurrenciesUseCase,
|
||||||
) : Model(), TokenDetailsClickIntents, YieldSupplyDepositedWarningComponent.ModelCallback {
|
) : Model(), TokenDetailsClickIntents, YieldSupplyDepositedWarningComponent.ModelCallback {
|
||||||
|
|
||||||
private val params = paramsContainer.require<TokenDetailsComponent.Params>()
|
private val params = paramsContainer.require<TokenDetailsComponent.Params>()
|
||||||
|
|
@ -158,6 +166,7 @@ internal class TokenDetailsModel @Inject constructor(
|
||||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
||||||
|
|
||||||
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
|
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
|
||||||
|
private var account: Account.CryptoPortfolio? = null
|
||||||
private var isBalanceLoadedEventSent = false
|
private var isBalanceLoadedEventSent = false
|
||||||
private var expressTxStatusTaskScheduler = SingleTaskScheduler<PersistentList<ExpressTransactionStateUM>>()
|
private var expressTxStatusTaskScheduler = SingleTaskScheduler<PersistentList<ExpressTransactionStateUM>>()
|
||||||
|
|
||||||
|
|
@ -228,15 +237,26 @@ internal class TokenDetailsModel @Inject constructor(
|
||||||
private fun initButtons() {
|
private fun initButtons() {
|
||||||
// we need also init buttons before start all loading to avoid buttons blocking
|
// we need also init buttons before start all loading to avoid buttons blocking
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
val currentCryptoCurrencyStatus = getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync(
|
val currentCryptoCurrencyStatus = if (accountsFeatureToggles.isFeatureEnabled) {
|
||||||
userWalletId = userWalletId,
|
getAccountCryptoCurrencyStatusUseCase.invokeSync(
|
||||||
cryptoCurrencyId = cryptoCurrency.id,
|
userWalletId = userWalletId,
|
||||||
isSingleWalletWithTokens = false,
|
currency = cryptoCurrency,
|
||||||
).getOrNull()
|
)
|
||||||
currentCryptoCurrencyStatus?.let {
|
.onSome { account = it.account }
|
||||||
cryptoCurrencyStatus = it
|
.getOrNull()
|
||||||
updateButtons(it)
|
?.status
|
||||||
updateWarnings(it)
|
} else {
|
||||||
|
getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync(
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
cryptoCurrencyId = cryptoCurrency.id,
|
||||||
|
isSingleWalletWithTokens = false,
|
||||||
|
).getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
currentCryptoCurrencyStatus?.let { status ->
|
||||||
|
cryptoCurrencyStatus = status
|
||||||
|
updateButtons(currencyStatus = status)
|
||||||
|
updateWarnings(cryptoCurrencyStatus = status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -308,12 +328,18 @@ internal class TokenDetailsModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun subscribeOnCurrencyStatusUpdates() {
|
private fun subscribeOnCurrencyStatusUpdates() {
|
||||||
getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
|
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||||
userWalletId = userWalletId,
|
getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency)
|
||||||
currencyId = cryptoCurrency.id,
|
.onEach { account = it.account }
|
||||||
isSingleWalletWithTokens = userWallet is UserWallet.Cold &&
|
.map { it.status.right() }
|
||||||
userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(),
|
} else {
|
||||||
)
|
getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
currencyId = cryptoCurrency.id,
|
||||||
|
isSingleWalletWithTokens = userWallet is UserWallet.Cold &&
|
||||||
|
userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(),
|
||||||
|
)
|
||||||
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.onEach { maybeCurrencyStatus ->
|
.onEach { maybeCurrencyStatus ->
|
||||||
internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus)
|
internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus)
|
||||||
|
|
@ -553,7 +579,7 @@ internal class TokenDetailsModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceiveClick(unavailabilityReason: ScenarioUnavailabilityReason) {
|
override fun onReceiveClick(unavailabilityReason: ScenarioUnavailabilityReason) {
|
||||||
val networkAddress = cryptoCurrencyStatus?.value?.networkAddress ?: return
|
cryptoCurrencyStatus?.value?.networkAddress ?: return
|
||||||
|
|
||||||
analyticsEventsHandler.send(
|
analyticsEventsHandler.send(
|
||||||
TokenScreenAnalyticsEvent.ButtonWithParams.ButtonReceive(
|
TokenScreenAnalyticsEvent.ButtonWithParams.ButtonReceive(
|
||||||
|
|
@ -689,7 +715,18 @@ internal class TokenDetailsModel @Inject constructor(
|
||||||
|
|
||||||
override fun onHideConfirmed() {
|
override fun onHideConfirmed() {
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
removeCurrencyUseCase.invoke(userWalletId, cryptoCurrency)
|
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||||
|
val accountId = account?.accountId
|
||||||
|
|
||||||
|
if (accountId == null) {
|
||||||
|
Timber.e("Account ID is null, cannot hide currency ${cryptoCurrency.id}")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrency)
|
||||||
|
} else {
|
||||||
|
removeCurrencyUseCase(userWalletId, cryptoCurrency)
|
||||||
|
}
|
||||||
.onLeft { Timber.e(it) }
|
.onLeft { Timber.e(it) }
|
||||||
.onRight { router.popBackStack() }
|
.onRight { router.popBackStack() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,17 @@ import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
|
||||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.datasource.local.swap.ExpressAnalyticsStatus
|
import com.tangem.datasource.local.swap.ExpressAnalyticsStatus
|
||||||
import com.tangem.datasource.local.swap.SwapTransactionStatusStore
|
import com.tangem.datasource.local.swap.SwapTransactionStatusStore
|
||||||
|
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||||
|
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||||
|
import com.tangem.domain.account.status.usecase.SaveCryptoCurrenciesUseCase
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
|
import com.tangem.domain.models.account.AccountId
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.quote.QuoteStatus
|
import com.tangem.domain.models.quote.QuoteStatus
|
||||||
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.quotes.QuotesRepository
|
import com.tangem.domain.quotes.QuotesRepository
|
||||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||||
import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent
|
import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
|
||||||
import com.tangem.feature.swap.domain.SwapTransactionRepository
|
import com.tangem.feature.swap.domain.SwapTransactionRepository
|
||||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||||
import com.tangem.feature.swap.domain.models.domain.*
|
import com.tangem.feature.swap.domain.models.domain.*
|
||||||
|
|
@ -27,6 +31,7 @@ import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.conflate
|
import kotlinx.coroutines.flow.conflate
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
internal class ExchangeStatusFactory @AssistedInject constructor(
|
internal class ExchangeStatusFactory @AssistedInject constructor(
|
||||||
|
|
@ -34,6 +39,9 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
|
||||||
private val swapRepository: SwapRepository,
|
private val swapRepository: SwapRepository,
|
||||||
private val quotesRepository: QuotesRepository,
|
private val quotesRepository: QuotesRepository,
|
||||||
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
|
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
|
||||||
|
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||||
|
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||||
|
private val saveCryptoCurrenciesUseCase: SaveCryptoCurrenciesUseCase,
|
||||||
private val swapTransactionStatusStore: SwapTransactionStatusStore,
|
private val swapTransactionStatusStore: SwapTransactionStatusStore,
|
||||||
private val analyticsEventsHandler: AnalyticsEventHandler,
|
private val analyticsEventsHandler: AnalyticsEventHandler,
|
||||||
@Assisted private val clickIntents: TokenDetailsClickIntents,
|
@Assisted private val clickIntents: TokenDetailsClickIntents,
|
||||||
|
|
@ -111,9 +119,41 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
|
||||||
ifRight = { statusModel ->
|
ifRight = { statusModel ->
|
||||||
sendStatusUpdateAnalytics(statusModel, provider)
|
sendStatusUpdateAnalytics(statusModel, provider)
|
||||||
|
|
||||||
val refundTokenCurrency = addRefundCurrencyIfNeeded(statusModel, provider.type)
|
val accountId = if (accountsFeatureToggles.isFeatureEnabled) {
|
||||||
|
getAccountCurrencyStatusUseCase.invokeSync(
|
||||||
|
userWalletId = userWallet.walletId,
|
||||||
|
currency = cryptoCurrency,
|
||||||
|
)
|
||||||
|
.map { it.account.accountId }
|
||||||
|
.getOrNull()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
swapTransactionRepository.storeTransactionState(txId, statusModel, refundTokenCurrency)
|
val refundTokenCurrency = if (accountsFeatureToggles.isFeatureEnabled) {
|
||||||
|
if (accountId != null) {
|
||||||
|
addRefundCurrencyIfNeededNew(
|
||||||
|
accountId = accountId,
|
||||||
|
status = statusModel,
|
||||||
|
type = provider.type,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Timber.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addRefundCurrencyIfNeededLegacy(status = statusModel, type = provider.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
swapTransactionRepository.storeTransactionState(
|
||||||
|
txId = txId,
|
||||||
|
status = statusModel,
|
||||||
|
accountWithCurrency = if (refundTokenCurrency != null) {
|
||||||
|
Pair(accountId, refundTokenCurrency)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
)
|
||||||
statusModel.copy(refundCurrency = refundTokenCurrency)
|
statusModel.copy(refundCurrency = refundTokenCurrency)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -135,7 +175,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
|
||||||
/**
|
/**
|
||||||
* For now do it only for dex-bridge provider
|
* For now do it only for dex-bridge provider
|
||||||
*/
|
*/
|
||||||
private suspend fun addRefundCurrencyIfNeeded(
|
private suspend fun addRefundCurrencyIfNeededLegacy(
|
||||||
status: ExchangeStatusModel?,
|
status: ExchangeStatusModel?,
|
||||||
type: ExchangeProviderType,
|
type: ExchangeProviderType,
|
||||||
): CryptoCurrency? {
|
): CryptoCurrency? {
|
||||||
|
|
@ -153,6 +193,27 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun addRefundCurrencyIfNeededNew(
|
||||||
|
accountId: AccountId,
|
||||||
|
status: ExchangeStatusModel?,
|
||||||
|
type: ExchangeProviderType,
|
||||||
|
): CryptoCurrency? {
|
||||||
|
status ?: return null
|
||||||
|
if (type != ExchangeProviderType.DEX_BRIDGE) return null
|
||||||
|
val refundNetwork = status.refundNetwork
|
||||||
|
val refundContractAddress = status.refundContractAddress
|
||||||
|
|
||||||
|
if (refundNetwork == null || refundContractAddress == null) return null
|
||||||
|
|
||||||
|
return saveCryptoCurrenciesUseCase.add(
|
||||||
|
accountId = accountId,
|
||||||
|
contractAddress = refundContractAddress,
|
||||||
|
networkId = refundNetwork,
|
||||||
|
)
|
||||||
|
.onLeft(Timber::e)
|
||||||
|
.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
private fun getExchangeStatusState(
|
private fun getExchangeStatusState(
|
||||||
savedTransactions: List<SavedSwapTransactionListModel>?,
|
savedTransactions: List<SavedSwapTransactionListModel>?,
|
||||||
quoteStatuses: Set<QuoteStatus>,
|
quoteStatuses: Set<QuoteStatus>,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ import com.tangem.core.ui.extensions.WrappedList
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.core.ui.haptic.TangemHapticEffect
|
import com.tangem.core.ui.haptic.TangemHapticEffect
|
||||||
import com.tangem.core.ui.haptic.VibratorHapticManager
|
import com.tangem.core.ui.haptic.VibratorHapticManager
|
||||||
|
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||||
|
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||||
|
import com.tangem.domain.account.status.usecase.SaveCryptoCurrenciesUseCase
|
||||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||||
import com.tangem.domain.appcurrency.extenstions.unwrap
|
import com.tangem.domain.appcurrency.extenstions.unwrap
|
||||||
import com.tangem.domain.core.lce.Lce
|
import com.tangem.domain.core.lce.Lce
|
||||||
|
|
@ -71,6 +74,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -151,6 +155,9 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
||||||
private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase,
|
private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase,
|
||||||
private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase,
|
private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase,
|
||||||
private val removeCurrencyUseCase: RemoveCurrencyUseCase,
|
private val removeCurrencyUseCase: RemoveCurrencyUseCase,
|
||||||
|
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||||
|
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||||
|
private val saveCryptoCurrenciesUseCase: SaveCryptoCurrenciesUseCase,
|
||||||
) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents {
|
) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents {
|
||||||
|
|
||||||
override fun onSendClick(
|
override fun onSendClick(
|
||||||
|
|
@ -340,7 +347,23 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
||||||
|
|
||||||
override fun onPerformHideToken(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) {
|
override fun onPerformHideToken(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) {
|
||||||
modelScope.launch(dispatchers.io) {
|
modelScope.launch(dispatchers.io) {
|
||||||
removeCurrencyUseCase(userWalletId, cryptoCurrencyStatus.currency)
|
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||||
|
val accountId = getAccountCurrencyStatusUseCase.invokeSync(
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
currency = cryptoCurrencyStatus.currency,
|
||||||
|
)
|
||||||
|
.map { it.account.accountId }
|
||||||
|
.getOrNull()
|
||||||
|
|
||||||
|
if (accountId == null) {
|
||||||
|
Timber.e("Account ID is null, cannot hide currency ${cryptoCurrencyStatus.currency.id}")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrencyStatus.currency)
|
||||||
|
} else {
|
||||||
|
removeCurrencyUseCase(userWalletId, cryptoCurrencyStatus.currency)
|
||||||
|
}
|
||||||
.fold(
|
.fold(
|
||||||
ifLeft = {
|
ifLeft = {
|
||||||
walletEventSender.send(
|
walletEventSender.send(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue