Updated on 2026-08-14
This commit is contained in:
commit
3cbc2dadfb
822 changed files with 12838 additions and 5366 deletions
|
|
@ -6,7 +6,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.WATCHED
|
|||
|
||||
sealed class StoriesEvents(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent("Stories", event, params) {
|
||||
|
||||
data class SwapStories(
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ private const val PROMO_CATEGORY = "Promo"
|
|||
|
||||
sealed class SwapEvents(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent(SWAP_CATEGORY, event, params) {
|
||||
|
||||
data class SwapScreenOpened(val token: String) : SwapEvents(
|
||||
|
|
@ -23,17 +23,17 @@ sealed class SwapEvents(
|
|||
params = mapOf("Token" to token),
|
||||
)
|
||||
|
||||
data object SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked")
|
||||
class SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked")
|
||||
|
||||
data class ChooseTokenScreenOpened(val availableTokens: Boolean) : SwapEvents(
|
||||
data class ChooseTokenScreenOpened(val hasAvailableTokens: Boolean) : SwapEvents(
|
||||
event = "Choose Token Screen Opened",
|
||||
params = mapOf("Available tokens" to if (availableTokens) "Yes" else "No"),
|
||||
params = mapOf("Available tokens" to if (hasAvailableTokens) "Yes" else "No"),
|
||||
)
|
||||
|
||||
data class ChooseTokenScreenResult(val tokenChosen: Boolean, val token: String? = null) : SwapEvents(
|
||||
data class ChooseTokenScreenResult(val isTokenChosen: Boolean, val token: String? = null) : SwapEvents(
|
||||
event = "Choose Token Screen Result",
|
||||
params = buildMap {
|
||||
put("Token Chosen", if (tokenChosen) "Yes" else "No")
|
||||
put("Token Chosen", if (isTokenChosen) "Yes" else "No")
|
||||
token?.let { put("Token", it) }
|
||||
},
|
||||
)
|
||||
|
|
@ -71,10 +71,11 @@ sealed class SwapEvents(
|
|||
),
|
||||
)
|
||||
|
||||
data object ButtonPermissionCancelClicked : SwapEvents(event = "Button - Permission Cancel")
|
||||
class ButtonPermissionCancelClicked : SwapEvents(event = "Button - Permission Cancel")
|
||||
|
||||
data object ButtonSwipeClicked : SwapEvents(event = "Button - Swipe")
|
||||
class ButtonSwipeClicked : SwapEvents(event = "Button - Swipe")
|
||||
|
||||
@Suppress("NullableToStringCall")
|
||||
data class SwapInProgressScreen(
|
||||
val provider: SwapProvider,
|
||||
val commission: FeeType, // Market / Fast
|
||||
|
|
@ -82,6 +83,8 @@ sealed class SwapEvents(
|
|||
val receiveBlockchain: String,
|
||||
val sendToken: String,
|
||||
val receiveToken: String,
|
||||
val fromDerivationIndex: Int?,
|
||||
val toDerivationIndex: Int?,
|
||||
) : SwapEvents(
|
||||
event = "Swap in Progress Screen Opened",
|
||||
params = mapOf(
|
||||
|
|
@ -91,10 +94,11 @@ sealed class SwapEvents(
|
|||
"Receive Token" to receiveToken,
|
||||
"Send Blockchain" to sendBlockchain,
|
||||
"Receive Blockchain" to receiveBlockchain,
|
||||
"Account Derivation From or To (optional)" to "$fromDerivationIndex, $toDerivationIndex",
|
||||
),
|
||||
)
|
||||
|
||||
data object ProviderClicked : SwapEvents("Provider Clicked")
|
||||
class ProviderClicked : SwapEvents("Provider Clicked")
|
||||
|
||||
data class ProviderChosen(val provider: SwapProvider) : SwapEvents(
|
||||
event = "Provider Chosen",
|
||||
|
|
@ -111,7 +115,7 @@ sealed class SwapEvents(
|
|||
params = mapOf("Token" to token),
|
||||
)
|
||||
|
||||
data object NoticeNoAvailableTokensToSwap : SwapEvents("Notice - No Available Tokens To Swap")
|
||||
class NoticeNoAvailableTokensToSwap : SwapEvents("Notice - No Available Tokens To Swap")
|
||||
|
||||
data class NoticeNotEnoughFee(val token: String, val blockchain: String) : SwapEvents(
|
||||
event = "Notice - Not Enough Fee",
|
||||
|
|
|
|||
|
|
@ -45,11 +45,11 @@ internal class AccountTokenItemConverter(
|
|||
fun createAvailableItemConverter(): TokenItemStateConverter {
|
||||
return TokenItemStateConverter(
|
||||
appCurrency = appCurrency,
|
||||
subtitleStateProvider = {
|
||||
subtitleStateProvider = { status ->
|
||||
createSubtitleState(
|
||||
status = it,
|
||||
status = status,
|
||||
isAvailable = true,
|
||||
text = stringReference(value = it.currency.symbol),
|
||||
text = stringReference(value = status.currency.symbol),
|
||||
)
|
||||
},
|
||||
subtitle2StateProvider = ::createSubtitle2State,
|
||||
|
|
@ -64,15 +64,15 @@ internal class AccountTokenItemConverter(
|
|||
return TokenItemStateConverter(
|
||||
appCurrency = appCurrency,
|
||||
iconStateProvider = { CryptoCurrencyToIconStateConverter(isAvailable = false).convert(it) },
|
||||
titleStateProvider = {
|
||||
titleStateProvider = { status ->
|
||||
TokenItemState.TitleState.Content(
|
||||
text = stringReference(value = it.currency.name),
|
||||
text = stringReference(value = status.currency.name),
|
||||
isAvailable = false,
|
||||
)
|
||||
},
|
||||
subtitleStateProvider = {
|
||||
subtitleStateProvider = { status ->
|
||||
createSubtitleState(
|
||||
status = it,
|
||||
status = status,
|
||||
isAvailable = false,
|
||||
text = unavailableErrorText,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ internal class TokensDataConverter(
|
|||
onSearchEntered = onSearchEntered,
|
||||
onTokenSelected = onTokenSelected,
|
||||
isBalanceHidden = isBalanceHiddenProvider(),
|
||||
afterSearch = group.isAfterSearch,
|
||||
isAfterSearch = group.isAfterSearch,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ internal class TokensDataConverter(
|
|||
id = cryptoCurrencyStatus.currency.id.value,
|
||||
name = cryptoCurrencyStatus.currency.name,
|
||||
symbol = cryptoCurrencyStatus.currency.symbol,
|
||||
available = isAvailable,
|
||||
isAvailable = isAvailable,
|
||||
tokenIcon = convertIcon(cryptoCurrencyStatus.currency, isAvailable),
|
||||
addedTokenBalanceData = TokenBalanceData(
|
||||
amount = formatCryptoAmount(cryptoCurrencyStatus),
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ internal class TokensDataConverterV2(
|
|||
onSearchEntered = onSearchEntered,
|
||||
onTokenSelected = onTokenSelected,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
afterSearch = tokensDataState.isAfterSearch,
|
||||
isAfterSearch = tokensDataState.isAfterSearch,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ internal class SwapModel @Inject constructor(
|
|||
private var initialToStatus: CryptoCurrencyStatus? = null
|
||||
|
||||
private var isBalanceHidden = true
|
||||
private var isAccountsMode: Boolean = false
|
||||
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
||||
|
||||
|
|
@ -151,7 +152,10 @@ internal class SwapModel @Inject constructor(
|
|||
)
|
||||
|
||||
private val inputNumberFormatter =
|
||||
InputNumberFormatter(NumberFormat.getInstance(Locale.getDefault()) as DecimalFormat)
|
||||
InputNumberFormatter(
|
||||
NumberFormat.getInstance(Locale.getDefault()) as? DecimalFormat
|
||||
?: error("NumberFormat is not DecimalFormat"),
|
||||
)
|
||||
private val amountDebouncer = Debouncer()
|
||||
private val singleTaskScheduler = SingleTaskScheduler<Map<SwapProvider, SwapState>>()
|
||||
|
||||
|
|
@ -170,18 +174,17 @@ internal class SwapModel @Inject constructor(
|
|||
private var isOrderReversed = false
|
||||
private val lastAmount = mutableStateOf(INITIAL_AMOUNT)
|
||||
private val lastReducedBalanceBy = mutableStateOf(BigDecimal.ZERO)
|
||||
private var swapRouter: SwapRouter = SwapRouter(router = router)
|
||||
private val swapRouter: SwapRouter = SwapRouter(router = router)
|
||||
private var userCountry: UserCountry? = null
|
||||
|
||||
private lateinit var fromAccountCurrencyStatus: AccountCryptoCurrencyStatus
|
||||
private var toAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null
|
||||
private var isAccountsMode: Boolean = false
|
||||
|
||||
private val isUserResolvableError: (SwapState) -> Boolean = {
|
||||
it is SwapState.SwapError &&
|
||||
private val isUserResolvableError: (SwapState) -> Boolean = { swapState ->
|
||||
swapState is SwapState.SwapError &&
|
||||
(
|
||||
it.error is ExpressDataError.ExchangeTooSmallAmountError ||
|
||||
it.error is ExpressDataError.ExchangeTooBigAmountError
|
||||
swapState.error is ExpressDataError.ExchangeTooSmallAmountError ||
|
||||
swapState.error is ExpressDataError.ExchangeTooBigAmountError
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -210,10 +213,10 @@ internal class SwapModel @Inject constructor(
|
|||
userWalletId = userWalletId,
|
||||
currency = initialCurrencyFrom,
|
||||
).getOrNull()
|
||||
val toAccountStatus = initialCurrencyTo?.let {
|
||||
val toAccountStatus = initialCurrencyTo?.let { currencyTo ->
|
||||
getAccountCurrencyStatusUseCase.invokeSync(
|
||||
userWalletId = userWalletId,
|
||||
currency = it,
|
||||
currency = currencyTo,
|
||||
).getOrNull()
|
||||
}
|
||||
|
||||
|
|
@ -228,10 +231,10 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
} else {
|
||||
val fromStatus = getFromStatus()
|
||||
val toStatus = initialCurrencyTo?.let {
|
||||
val toStatus = initialCurrencyTo?.let { currencyTo ->
|
||||
getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyId = it.id,
|
||||
cryptoCurrencyId = currencyTo.id,
|
||||
).getOrNull()
|
||||
}
|
||||
|
||||
|
|
@ -248,8 +251,8 @@ internal class SwapModel @Inject constructor(
|
|||
analyticsEventHandler.send(SwapEvents.SwapScreenOpened(initialCurrencyFrom.symbol))
|
||||
|
||||
getBalanceHidingSettingsUseCase()
|
||||
.onEach {
|
||||
isBalanceHidden = it.isBalanceHidden
|
||||
.onEach { settings ->
|
||||
isBalanceHidden = settings.isBalanceHidden
|
||||
uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
|
@ -275,7 +278,7 @@ internal class SwapModel @Inject constructor(
|
|||
val isAnyAvailableAccountTokensFrom = !dataState.tokensDataState?.fromGroup?.accountCurrencyList.isNullOrEmpty()
|
||||
val isAnyAvailableTokens = isAnyAvailableTokensTo || isAnyAvailableTokensFrom ||
|
||||
isAnyAvailableAccountTokensTo || isAnyAvailableAccountTokensFrom
|
||||
analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(availableTokens = isAnyAvailableTokens))
|
||||
analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(hasAvailableTokens = isAnyAvailableTokens))
|
||||
}
|
||||
|
||||
private fun initTokens(isReverseFromTo: Boolean) {
|
||||
|
|
@ -290,10 +293,10 @@ internal class SwapModel @Inject constructor(
|
|||
initialCryptoCurrency = initialCurrencyFrom,
|
||||
state = state,
|
||||
isReverseFromTo = isReverseFromTo,
|
||||
)?.let {
|
||||
)?.let { accountSwapCurrency ->
|
||||
AccountCryptoCurrencyStatus(
|
||||
account = it.account,
|
||||
status = it.cryptoCurrencyStatus,
|
||||
account = accountSwapCurrency.account,
|
||||
status = accountSwapCurrency.cryptoCurrencyStatus,
|
||||
)
|
||||
}
|
||||
selectedAccountCurrency?.status to selectedAccountCurrency?.account
|
||||
|
|
@ -313,23 +316,23 @@ internal class SwapModel @Inject constructor(
|
|||
isReverseFromTo = isReverseFromTo,
|
||||
)
|
||||
|
||||
(dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let {
|
||||
(dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { coin ->
|
||||
subscribeToCoinBalanceUpdates(
|
||||
userWalletId = userWalletId,
|
||||
coin = it,
|
||||
coin = coin,
|
||||
isFromCurrency = true,
|
||||
)
|
||||
}
|
||||
|
||||
(dataState.toCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let {
|
||||
(dataState.toCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { coin ->
|
||||
subscribeToCoinBalanceUpdates(
|
||||
userWalletId = userWalletId,
|
||||
coin = it,
|
||||
coin = coin,
|
||||
isFromCurrency = false,
|
||||
)
|
||||
}
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}.onFailure { error ->
|
||||
Timber.e(error)
|
||||
|
||||
applyInitialTokenChoice(
|
||||
state = TokensDataStateExpress.EMPTY,
|
||||
|
|
@ -340,7 +343,7 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
uiState = stateBuilder.createInitialErrorState(
|
||||
uiState,
|
||||
(it as? ExpressException)?.expressDataError?.code ?: ExpressDataError.UnknownError.code,
|
||||
(error as? ExpressException)?.expressDataError?.code ?: ExpressDataError.UnknownError.code,
|
||||
) {
|
||||
uiState = stateBuilder.createInitialLoadingState(
|
||||
initialCurrencyFrom = initialCurrencyFrom,
|
||||
|
|
@ -376,7 +379,7 @@ internal class SwapModel @Inject constructor(
|
|||
) {
|
||||
// exceptional case
|
||||
if (selectedCurrency == null) {
|
||||
analyticsEventHandler.send(SwapEvents.NoticeNoAvailableTokensToSwap)
|
||||
analyticsEventHandler.send(SwapEvents.NoticeNoAvailableTokensToSwap())
|
||||
uiState = stateBuilder.createNoAvailableTokensToSwapState(
|
||||
uiStateHolder = uiState,
|
||||
fromToken = initialFromStatus,
|
||||
|
|
@ -389,7 +392,7 @@ internal class SwapModel @Inject constructor(
|
|||
} else {
|
||||
initialFromStatus to selectedCurrency
|
||||
}
|
||||
val (fromAccount, toAccount) = if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
val (fromAccount, toAccount) = if (accountsFeatureToggles.isFeatureEnabled && tangemPayInput == null) {
|
||||
if (isOrderReversed) {
|
||||
selectedAccount to fromAccountCurrencyStatus.account
|
||||
} else {
|
||||
|
|
@ -497,7 +500,7 @@ internal class SwapModel @Inject constructor(
|
|||
toProvidersList: List<SwapProvider>,
|
||||
): PeriodicTask<Map<SwapProvider, SwapState>> {
|
||||
return PeriodicTask(
|
||||
UPDATE_DELAY,
|
||||
delay = UPDATE_DELAY,
|
||||
task = {
|
||||
uiState = stateBuilder.createSilentLoadState(uiState)
|
||||
runCatching(dispatchers.io) {
|
||||
|
|
@ -535,8 +538,8 @@ internal class SwapModel @Inject constructor(
|
|||
Timber.e("Accidentally empty quotes list")
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
Timber.e("Error when loading quotes: $it")
|
||||
onError = { error ->
|
||||
Timber.e("Error when loading quotes: $error")
|
||||
uiState = stateBuilder.addNotification(uiState, null) { startLoadingQuotesFromLastState() }
|
||||
},
|
||||
)
|
||||
|
|
@ -641,13 +644,13 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun sendErrorAnalyticsEvent(error: ExpressDataError, provider: SwapProvider) {
|
||||
val receiveToken = dataState.toCryptoCurrency?.currency?.let {
|
||||
"${it.network.backendId}:${it.symbol}"
|
||||
val receiveToken = dataState.toCryptoCurrency?.currency?.let { currency ->
|
||||
"${currency.network.backendId}:${currency.symbol}"
|
||||
}
|
||||
analyticsErrorEventHandler.sendErrorEvent(
|
||||
SwapEvents.NoticeProviderError(
|
||||
sendToken = "${initialCurrencyFrom.network.backendId}:${initialCurrencyFrom.symbol}",
|
||||
receiveToken = receiveToken ?: "",
|
||||
receiveToken = receiveToken.orEmpty(),
|
||||
provider = provider,
|
||||
errorCode = error.code,
|
||||
errorMessage = error.message,
|
||||
|
|
@ -656,7 +659,7 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun updateLoadedQuotes(state: Map<SwapProvider, SwapState>): Pair<SwapProvider, SwapState> {
|
||||
val nonEmptyStates = state.filter { it.value !is SwapState.EmptyAmountState }
|
||||
val nonEmptyStates = state.filter { entry -> entry.value !is SwapState.EmptyAmountState }
|
||||
val selectedSwapProvider = if (nonEmptyStates.isNotEmpty()) {
|
||||
selectProvider(state)
|
||||
} else {
|
||||
|
|
@ -666,8 +669,8 @@ internal class SwapModel @Inject constructor(
|
|||
selectedProvider = selectedSwapProvider,
|
||||
lastLoadedSwapStates = state,
|
||||
)
|
||||
selectedSwapProvider?.let {
|
||||
return nonEmptyStates.entries.first { it.key == selectedSwapProvider }.toPair()
|
||||
if (selectedSwapProvider != null) {
|
||||
return nonEmptyStates.entries.first { entry -> entry.key == selectedSwapProvider }.toPair()
|
||||
}
|
||||
return state.entries.first().toPair()
|
||||
}
|
||||
|
|
@ -832,8 +835,8 @@ internal class SwapModel @Inject constructor(
|
|||
processTangemPayWithdrawal(swapTransactionState = swapTransactionState)
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}.onFailure { error ->
|
||||
Timber.e(error)
|
||||
startLoadingQuotesFromLastState()
|
||||
makeDefaultAlert()
|
||||
}
|
||||
|
|
@ -889,6 +892,8 @@ internal class SwapModel @Inject constructor(
|
|||
val fee = dataState.selectedFee?.feeType ?: return
|
||||
val fromCurrency = dataState.fromCryptoCurrency?.currency ?: return
|
||||
val toCurrency = dataState.toCryptoCurrency?.currency ?: return
|
||||
val fromDerivationIndex = dataState.fromAccount?.derivationIndex?.value
|
||||
val toDerivationIndex = dataState.toAccount?.derivationIndex?.value
|
||||
|
||||
analyticsEventHandler.send(
|
||||
SwapEvents.SwapInProgressScreen(
|
||||
|
|
@ -898,6 +903,8 @@ internal class SwapModel @Inject constructor(
|
|||
receiveBlockchain = toCurrency.network.name,
|
||||
sendToken = fromCurrency.symbol,
|
||||
receiveToken = toCurrency.symbol,
|
||||
fromDerivationIndex = fromDerivationIndex,
|
||||
toDerivationIndex = toDerivationIndex,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -905,7 +912,7 @@ internal class SwapModel @Inject constructor(
|
|||
@Suppress("LongMethod")
|
||||
private fun givePermissionsToSwap() {
|
||||
modelScope.launch(dispatchers.main) {
|
||||
runCatching {
|
||||
runSuspendCatching {
|
||||
val fromCryptoCurrency = requireNotNull(dataState.fromCryptoCurrency) {
|
||||
"dataState.fromCryptoCurrency might not be null"
|
||||
}
|
||||
|
|
@ -922,7 +929,7 @@ internal class SwapModel @Inject constructor(
|
|||
TxFeeState.Empty -> {
|
||||
makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text))
|
||||
Timber.e("Fee should not be Empty")
|
||||
return@runCatching
|
||||
return@launch
|
||||
}
|
||||
is TxFeeState.MultipleFeeState -> fee.priorityFee
|
||||
is TxFeeState.SingleFeeState -> fee.fee
|
||||
|
|
@ -969,8 +976,8 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}.onFailure { makeDefaultAlert() }
|
||||
}.onFailure {
|
||||
Timber.e(it.message.orEmpty())
|
||||
}.onFailure { error ->
|
||||
Timber.e(error.message.orEmpty())
|
||||
makeDefaultAlert()
|
||||
}
|
||||
}
|
||||
|
|
@ -985,13 +992,13 @@ internal class SwapModel @Inject constructor(
|
|||
tokenDataState.toGroup
|
||||
}
|
||||
|
||||
val available = group.available.filter {
|
||||
it.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) ||
|
||||
it.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true)
|
||||
val available = group.available.filter { swapAvailability ->
|
||||
swapAvailability.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) ||
|
||||
swapAvailability.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
val unavailable = group.unavailable.filter {
|
||||
it.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) ||
|
||||
it.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true)
|
||||
val unavailable = group.unavailable.filter { swapAvailability ->
|
||||
swapAvailability.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) ||
|
||||
swapAvailability.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
val accountCurrencyList = group.accountCurrencyList.mapNotNull { accountSwapAvailability ->
|
||||
val filteredCurrencies = accountSwapAvailability.currencyList.filter { accountSwapCurrency ->
|
||||
|
|
@ -1036,8 +1043,8 @@ internal class SwapModel @Inject constructor(
|
|||
val tokens = dataState.tokensDataState ?: return
|
||||
val (foundToken, foundAccount) = getSelectedTokenAndAccount(tokens, id)
|
||||
|
||||
foundToken?.currency?.symbol?.let {
|
||||
analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(tokenChosen = true, token = it))
|
||||
foundToken?.currency?.symbol?.let { symbol ->
|
||||
analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = symbol))
|
||||
}
|
||||
|
||||
if (foundToken != null) {
|
||||
|
|
@ -1117,8 +1124,10 @@ internal class SwapModel @Inject constructor(
|
|||
tokens.fromGroup
|
||||
} else {
|
||||
tokens.toGroup
|
||||
}.accountCurrencyList.firstNotNullOfOrNull {
|
||||
it.currencyList.firstOrNull { it.cryptoCurrencyStatus.currency.id.value == id }
|
||||
}.accountCurrencyList.firstNotNullOfOrNull { accountSwapAvailability ->
|
||||
accountSwapAvailability.currencyList.firstOrNull { accountSwapCurrency ->
|
||||
accountSwapCurrency.cryptoCurrencyStatus.currency.id.value == id
|
||||
}
|
||||
}
|
||||
accountCryptoCurrencyStatus?.cryptoCurrencyStatus to accountCryptoCurrencyStatus?.account
|
||||
} else {
|
||||
|
|
@ -1126,7 +1135,9 @@ internal class SwapModel @Inject constructor(
|
|||
tokens.fromGroup
|
||||
} else {
|
||||
tokens.toGroup
|
||||
}.available.firstOrNull { it.currencyStatus.currency.id.value == id }?.currencyStatus to null
|
||||
}.available.firstOrNull { swapAvailability ->
|
||||
swapAvailability.currencyStatus.currency.id.value == id
|
||||
}?.currencyStatus to null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1143,7 +1154,7 @@ internal class SwapModel @Inject constructor(
|
|||
currency = coin,
|
||||
).distinctUntilChanged { old, new -> old.status.value.amount == new.status.value.amount } // Check only balance changes
|
||||
.onEach { (account, currencyStatus) ->
|
||||
Timber.d("${coin.id} balance is ${currencyStatus.value.amount}")
|
||||
Timber.d("${coin.id} balance is ${currencyStatus.value.amount ?: "null"}")
|
||||
|
||||
dataState = dataState.copy(
|
||||
feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase(
|
||||
|
|
@ -1173,24 +1184,24 @@ internal class SwapModel @Inject constructor(
|
|||
userWalletId = userWalletId,
|
||||
currencyId = coin.id,
|
||||
isSingleWalletWithTokens = false,
|
||||
).mapNotNull { (it as? Either.Right)?.value }
|
||||
).mapNotNull { either -> (either as? Either.Right)?.value }
|
||||
.distinctUntilChanged { old, new -> old.value.amount == new.value.amount } // Check only balance changes
|
||||
.onEach {
|
||||
Timber.d("${coin.id} balance is ${it.value.amount}")
|
||||
.onEach { status ->
|
||||
Timber.d("${coin.id} balance is ${status.value.amount ?: "null"}")
|
||||
|
||||
dataState = dataState.copy(
|
||||
feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyStatus = it,
|
||||
).getOrNull() ?: it,
|
||||
cryptoCurrencyStatus = status,
|
||||
).getOrNull() ?: status,
|
||||
)
|
||||
|
||||
uiState = if (isFromCurrency) {
|
||||
dataState = dataState.copy(fromCryptoCurrency = it)
|
||||
stateBuilder.updateSendCurrencyBalance(uiState, it)
|
||||
dataState = dataState.copy(fromCryptoCurrency = status)
|
||||
stateBuilder.updateSendCurrencyBalance(uiState, status)
|
||||
} else {
|
||||
dataState = dataState.copy(toCryptoCurrency = it)
|
||||
stateBuilder.updateReceiveCurrencyBalance(uiState, it)
|
||||
dataState = dataState.copy(toCryptoCurrency = status)
|
||||
stateBuilder.updateReceiveCurrencyBalance(uiState, status)
|
||||
}
|
||||
|
||||
startLoadingQuotesFromLastState(isSilent = true)
|
||||
|
|
@ -1217,8 +1228,8 @@ internal class SwapModel @Inject constructor(
|
|||
toAccount = newToAccount,
|
||||
)
|
||||
isOrderReversed = !isOrderReversed
|
||||
dataState.tokensDataState?.let {
|
||||
updateTokensState(it)
|
||||
dataState.tokensDataState?.let { tokensDataState ->
|
||||
updateTokensState(tokensDataState)
|
||||
}
|
||||
|
||||
val minTxAmount = getMinimumTransactionAmountSyncUseCase(
|
||||
|
|
@ -1297,8 +1308,8 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onMaxAmountClicked() {
|
||||
dataState.fromCryptoCurrency?.let {
|
||||
val balance = swapInteractor.getTokenBalance(it)
|
||||
dataState.fromCryptoCurrency?.let { fromCurrency ->
|
||||
val balance = swapInteractor.getTokenBalance(fromCurrency)
|
||||
onAmountChanged(balance.formatToUIRepresentation())
|
||||
}
|
||||
}
|
||||
|
|
@ -1313,7 +1324,7 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
private fun onAmountSelected(selected: Boolean) {
|
||||
if (selected) {
|
||||
analyticsEventHandler.send(SwapEvents.SendTokenBalanceClicked)
|
||||
analyticsEventHandler.send(SwapEvents.SendTokenBalanceClicked())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1354,7 +1365,7 @@ internal class SwapModel @Inject constructor(
|
|||
},
|
||||
onChangeCardsClicked = {
|
||||
onChangeCardsClicked()
|
||||
analyticsEventHandler.send(SwapEvents.ButtonSwipeClicked)
|
||||
analyticsEventHandler.send(SwapEvents.ButtonSwipeClicked())
|
||||
},
|
||||
onBackClicked = {
|
||||
val bottomSheet = uiState.bottomSheetConfig
|
||||
|
|
@ -1362,7 +1373,7 @@ internal class SwapModel @Inject constructor(
|
|||
uiState = stateBuilder.dismissBottomSheet(uiState)
|
||||
} else {
|
||||
if (swapRouter.currentScreen == SwapNavScreen.SelectToken) {
|
||||
analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(tokenChosen = false))
|
||||
analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(isTokenChosen = false))
|
||||
}
|
||||
swapRouter.back()
|
||||
}
|
||||
|
|
@ -1376,7 +1387,7 @@ internal class SwapModel @Inject constructor(
|
|||
sendGivePermissionClickedEvent()
|
||||
uiState = stateBuilder.showPermissionBottomSheet(uiState) {
|
||||
startLoadingQuotesFromLastState(isSilent = true)
|
||||
analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked)
|
||||
analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked())
|
||||
uiState = stateBuilder.dismissBottomSheet(uiState)
|
||||
}
|
||||
},
|
||||
|
|
@ -1396,15 +1407,15 @@ internal class SwapModel @Inject constructor(
|
|||
uiState = stateBuilder.dismissBottomSheet(uiState)
|
||||
}
|
||||
},
|
||||
onSelectFeeType = {
|
||||
onSelectFeeType = { feeType ->
|
||||
uiState = stateBuilder.dismissBottomSheet(uiState)
|
||||
dataState = dataState.copy(selectedFee = it)
|
||||
dataState = dataState.copy(selectedFee = feeType)
|
||||
modelScope.launch(dispatchers.io) {
|
||||
startLoadingQuotesFromLastState(false)
|
||||
}
|
||||
},
|
||||
onProviderClick = { providerId ->
|
||||
analyticsEventHandler.send(SwapEvents.ProviderClicked)
|
||||
analyticsEventHandler.send(SwapEvents.ProviderClicked())
|
||||
val states = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates()
|
||||
val pricesLowerBest = getPricesLowerBest(providerId, states)
|
||||
uiState = stateBuilder.showSelectProviderBottomSheet(
|
||||
|
|
@ -1415,8 +1426,8 @@ internal class SwapModel @Inject constructor(
|
|||
needApplyFCARestrictions = userCountry.needApplyFCARestrictions(),
|
||||
) { uiState = stateBuilder.dismissBottomSheet(uiState) }
|
||||
},
|
||||
onProviderSelect = {
|
||||
val provider = findAndSelectProvider(it)
|
||||
onProviderSelect = { providerId ->
|
||||
val provider = findAndSelectProvider(providerId)
|
||||
val swapState = dataState.lastLoadedSwapStates[provider]
|
||||
val fromToken = dataState.fromCryptoCurrency
|
||||
if (provider != null && swapState != null && fromToken != null) {
|
||||
|
|
@ -1516,11 +1527,14 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
private fun findBestQuoteProvider(state: SuccessLoadedSwapData): SwapProvider? {
|
||||
// finding best quotes
|
||||
return state.minByOrNull {
|
||||
if (!it.value.fromTokenInfo.amountFiat.isNullOrZero() && !it.value.toTokenInfo.amountFiat.isNullOrZero()) {
|
||||
it.value.fromTokenInfo.amountFiat.divide(
|
||||
it.value.toTokenInfo.amountFiat,
|
||||
it.value.toTokenInfo.cryptoCurrencyStatus.currency.decimals,
|
||||
return state.minByOrNull { entry ->
|
||||
val toTokenInfo = entry.value.toTokenInfo
|
||||
val fromAmountFiat = entry.value.fromTokenInfo.amountFiat
|
||||
val toAmountFiat = toTokenInfo.amountFiat
|
||||
if (!fromAmountFiat.isNullOrZero() && !toAmountFiat.isNullOrZero()) {
|
||||
fromAmountFiat.divide(
|
||||
toAmountFiat,
|
||||
toTokenInfo.cryptoCurrencyStatus.currency.decimals,
|
||||
RoundingMode.HALF_UP,
|
||||
)
|
||||
} else {
|
||||
|
|
@ -1530,17 +1544,19 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun getPricesLowerBest(selectedProviderId: String, state: SuccessLoadedSwapData): Map<String, Float> {
|
||||
val selectedProviderEntry = state.filter { it.key.providerId == selectedProviderId }.entries.firstOrNull()
|
||||
?: return emptyMap()
|
||||
val selectedProviderEntry = state
|
||||
.filter { entry -> entry.key.providerId == selectedProviderId }
|
||||
.entries
|
||||
.firstOrNull() ?: return emptyMap()
|
||||
val selectedProviderRate = selectedProviderEntry.value.toTokenInfo.tokenAmount.value
|
||||
val hundredPercent = BigDecimal("100")
|
||||
return state.entries.mapNotNull {
|
||||
if (it.key != selectedProviderEntry.key) {
|
||||
val amount = it.value.toTokenInfo.tokenAmount.value
|
||||
return state.entries.mapNotNull { entry ->
|
||||
if (entry.key != selectedProviderEntry.key) {
|
||||
val amount = entry.value.toTokenInfo.tokenAmount.value
|
||||
val percentDiff = BigDecimal.ONE.minus(
|
||||
selectedProviderRate.divide(amount, RoundingMode.HALF_UP),
|
||||
).multiply(hundredPercent)
|
||||
it.key.providerId to percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat()
|
||||
entry.key.providerId to percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
@ -1572,11 +1588,15 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
return if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
groupToFind.accountCurrencyList.firstNotNullOfOrNull { (_, currencyList) ->
|
||||
currencyList.find { idToFind == it.cryptoCurrencyStatus.currency.id.value && it.isAvailable }
|
||||
currencyList.find { accountSwapCurrency ->
|
||||
idToFind == accountSwapCurrency.cryptoCurrencyStatus.currency.id.value &&
|
||||
accountSwapCurrency.isAvailable
|
||||
}
|
||||
}?.providers
|
||||
} else {
|
||||
groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value }
|
||||
?.providers
|
||||
groupToFind.available.find { swapAvailability ->
|
||||
idToFind == swapAvailability.currencyStatus.currency.id.value
|
||||
}?.providers
|
||||
}
|
||||
?.filterForTangemPayWithdrawal()
|
||||
.orEmpty()
|
||||
|
|
@ -1591,13 +1611,13 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun Map<SwapProvider, SwapState>.getLastLoadedSuccessStates(): SuccessLoadedSwapData {
|
||||
return this.filter { it.value is SwapState.QuotesLoadedState }
|
||||
.mapValues { it.value as SwapState.QuotesLoadedState }
|
||||
return this.filter { entry -> entry.value is SwapState.QuotesLoadedState }
|
||||
.mapValues { entry -> entry.value as SwapState.QuotesLoadedState }
|
||||
}
|
||||
|
||||
private fun Map<SwapProvider, SwapState>.consideredProvidersStates(): Map<SwapProvider, SwapState> {
|
||||
return this.filter {
|
||||
it.value is SwapState.QuotesLoadedState || isUserResolvableError(it.value)
|
||||
return this.filter { entry ->
|
||||
entry.value is SwapState.QuotesLoadedState || isUserResolvableError(entry.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1615,10 +1635,14 @@ internal class SwapModel @Inject constructor(
|
|||
val chosen = if (isOrderReversed) from else to
|
||||
|
||||
return if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
currenciesGroup.accountCurrencyList.flatMap { it.currencyList.map { it.cryptoCurrencyStatus } }
|
||||
currenciesGroup.accountCurrencyList.flatMap { accountSwapAvailability ->
|
||||
accountSwapAvailability.currencyList.map { accountSwapCurrency ->
|
||||
accountSwapCurrency.cryptoCurrencyStatus
|
||||
}
|
||||
}
|
||||
} else {
|
||||
currenciesGroup.available.map { it.currencyStatus }
|
||||
}.map { it.currency }.contains(chosen.currency)
|
||||
currenciesGroup.available.map { swapAvailability -> swapAvailability.currencyStatus }
|
||||
}.map { currencyStatus -> currencyStatus.currency }.contains(chosen.currency)
|
||||
}
|
||||
|
||||
private fun sendNoticePermissionNeededEvent() {
|
||||
|
|
|
|||
|
|
@ -125,9 +125,10 @@ internal class SwapNotificationsFactory(
|
|||
if (quoteModel.permissionState is PermissionDataState.PermissionLoading) {
|
||||
add(SwapNotificationUM.Error.ApprovalInProgressWarning)
|
||||
} else if (quoteModel.preparedSwapConfigState.hasOutgoingTransaction) {
|
||||
val fromCurrency = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency
|
||||
add(
|
||||
SwapNotificationUM.Error.TransactionInProgressWarning(
|
||||
currencySymbol = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency.network.currencySymbol,
|
||||
currencySymbol = fromCurrency.network.currencySymbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -290,10 +291,10 @@ internal class SwapNotificationsFactory(
|
|||
) {
|
||||
if (hideFee) return
|
||||
val feeEnoughState = quoteModel.preparedSwapConfigState.feeState as? SwapFeeState.NotEnough ?: return
|
||||
val needShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough &&
|
||||
val shouldShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough &&
|
||||
quoteModel.permissionState !is PermissionDataState.PermissionLoading &&
|
||||
feeEnoughState.feeCurrency != fromToken
|
||||
if (needShowCoverWarning) {
|
||||
if (shouldShowCoverWarning) {
|
||||
add(
|
||||
SwapNotificationUM.Error.UnableToCoverFeeWarning(
|
||||
fromToken = fromToken,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ internal data class SwapSelectTokenStateHolder(
|
|||
val unavailableTokens: ImmutableList<TokenToSelectState>,
|
||||
val tokensListData: TokenListUMData,
|
||||
val isBalanceHidden: Boolean,
|
||||
val afterSearch: Boolean,
|
||||
val isAfterSearch: Boolean,
|
||||
val onSearchEntered: (String) -> Unit,
|
||||
val onTokenSelected: (String) -> Unit,
|
||||
)
|
||||
|
|
@ -25,7 +25,7 @@ internal sealed class TokenToSelectState {
|
|||
val name: String,
|
||||
val symbol: String,
|
||||
val tokenIcon: CurrencyIconState,
|
||||
val available: Boolean = true,
|
||||
val isAvailable: Boolean = true,
|
||||
val addedTokenBalanceData: TokenBalanceData? = null,
|
||||
) : TokenToSelectState()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ sealed class SwapCardState {
|
|||
|
||||
data class SwapButton(
|
||||
@DrawableRes val walletInteractionIcon: Int?,
|
||||
val enabled: Boolean,
|
||||
val isEnabled: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
||||
|
|
@ -94,7 +94,7 @@ sealed interface TransactionCardType {
|
|||
) : TransactionCardType
|
||||
|
||||
data class ReadOnly(
|
||||
val showWarning: Boolean = false,
|
||||
val shouldShowWarning: Boolean = false,
|
||||
val onWarningClick: (() -> Unit)? = null,
|
||||
override val inputError: InputError = InputError.Empty,
|
||||
override val accountTitleUM: AccountTitleUM? = null,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ data class SwapSuccessStateHolder(
|
|||
val txUrl: String,
|
||||
val fee: TextReference,
|
||||
val rate: TextReference,
|
||||
val showStatusButton: Boolean,
|
||||
val shouldShowStatusButton: Boolean,
|
||||
val providerName: TextReference,
|
||||
val providerType: TextReference,
|
||||
val providerIcon: String,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ internal data object SwapSuccessStatePreview {
|
|||
fee = TextReference.Str("1 000 DAI ~ 1 000 MATIC"),
|
||||
providerName = TextReference.Str("1inch"),
|
||||
providerType = TextReference.Str(ExchangeProviderType.DEX.providerName),
|
||||
showStatusButton = false,
|
||||
shouldShowStatusButton = false,
|
||||
providerIcon = "",
|
||||
fromTitle = AccountTitleUM.Account(
|
||||
prefixText = stringReference("From"),
|
||||
|
|
|
|||
|
|
@ -58,14 +58,13 @@ private fun ChooseFeeBottomSheetContent(content: ChooseFeeBottomSheetConfig) {
|
|||
}
|
||||
FooterBlock(
|
||||
readMore = content.readMore,
|
||||
readMoreUrl = content.readMoreUrl,
|
||||
onReadMoreClick = content.onReadMoreClick,
|
||||
onReadMoreClick = { content.onReadMoreClick(content.readMoreUrl) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FooterBlock(readMore: TextReference, readMoreUrl: String, onReadMoreClick: (String) -> Unit) {
|
||||
private fun FooterBlock(readMore: TextReference, onReadMoreClick: () -> Unit) {
|
||||
val linkText = readMore.resolveReference()
|
||||
val fullString = stringResourceSafe(R.string.common_fee_selector_footer, linkText)
|
||||
val linkTextPosition = fullString.length - linkText.length
|
||||
|
|
@ -81,7 +80,7 @@ private fun FooterBlock(readMore: TextReference, readMoreUrl: String, onReadMore
|
|||
val click = { i: Int ->
|
||||
val readMoreStyle = requireNotNull(annotatedString.spanStyles.getOrNull(1))
|
||||
if (i in readMoreStyle.start..readMoreStyle.end) {
|
||||
onReadMoreClick(readMoreUrl)
|
||||
onReadMoreClick()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,7 +101,7 @@ private fun FooterBlock(readMore: TextReference, readMoreUrl: String, onReadMore
|
|||
private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) {
|
||||
content.feeItems.forEachIndexed { index, feeItem ->
|
||||
val isSelected = feeItem.feeType == content.selectedFee
|
||||
val showDivider = content.feeItems.lastIndex != index
|
||||
val shouldShowDivider = content.feeItems.lastIndex != index
|
||||
val symbol = " ${feeItem.symbolCrypto}"
|
||||
val preDotText = "${feeItem.amountCrypto}$symbol"
|
||||
val postDot = feeItem.amountFiatFormatted
|
||||
|
|
@ -117,7 +116,7 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) {
|
|||
ellipsizeOffset = ellipsizeOffset,
|
||||
isSelected = isSelected,
|
||||
onSelect = { content.onSelectFeeType(feeItem.feeType) },
|
||||
showDivider = showDivider,
|
||||
showDivider = shouldShowDivider,
|
||||
)
|
||||
}
|
||||
FeeType.PRIORITY -> {
|
||||
|
|
@ -129,7 +128,7 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) {
|
|||
ellipsizeOffset = ellipsizeOffset,
|
||||
isSelected = isSelected,
|
||||
onSelect = { content.onSelectFeeType(feeItem.feeType) },
|
||||
showDivider = showDivider,
|
||||
showDivider = shouldShowDivider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,16 +132,16 @@ private fun ProviderContentState(
|
|||
modifier = Modifier.padding(end = TangemTheme.dimens.spacing4),
|
||||
)
|
||||
}
|
||||
AnimatedContent(targetState = state.name, label = "") {
|
||||
AnimatedContent(targetState = state.name, label = "") { name ->
|
||||
Text(
|
||||
text = it,
|
||||
text = name,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
AnimatedContent(targetState = state.type, label = "") {
|
||||
AnimatedContent(targetState = state.type, label = "") { type ->
|
||||
Text(
|
||||
text = it,
|
||||
text = type,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
|
||||
|
|
@ -162,9 +162,9 @@ private fun ProviderContentState(
|
|||
end = TangemTheme.dimens.spacing56,
|
||||
),
|
||||
) {
|
||||
AnimatedContent(targetState = state.subtitle, label = "") {
|
||||
AnimatedContent(targetState = state.subtitle, label = "") { subtitle ->
|
||||
Text(
|
||||
text = it.resolveReference(),
|
||||
text = subtitle.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
|
@ -179,9 +179,9 @@ private fun ProviderContentState(
|
|||
} else {
|
||||
TangemTheme.colors.text.warning
|
||||
}
|
||||
AnimatedContent(targetState = state.percentLowerThenBest.value, label = "") {
|
||||
AnimatedContent(targetState = state.percentLowerThenBest.value, label = "") { percentValue ->
|
||||
Text(
|
||||
text = if (it > 0) "+$it%" else "$it%",
|
||||
text = if (percentValue > 0) "+$percentValue%" else "$percentValue%",
|
||||
style = TangemTheme.typography.body2,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
|
||||
|
|
@ -231,25 +231,25 @@ private fun ProviderUnavailableState(
|
|||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Row {
|
||||
AnimatedContent(targetState = state.name, label = "") {
|
||||
AnimatedContent(targetState = state.name, label = "") { name ->
|
||||
Text(
|
||||
text = it,
|
||||
text = name,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
AnimatedContent(targetState = state.type, label = "") {
|
||||
AnimatedContent(targetState = state.type, label = "") { type ->
|
||||
Text(
|
||||
text = it,
|
||||
text = type,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedContent(targetState = state.alertText, label = "") {
|
||||
AnimatedContent(targetState = state.alertText, label = "") { alertText ->
|
||||
Text(
|
||||
text = it.resolveReference(),
|
||||
text = alertText.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing6),
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ internal class StateBuilder(
|
|||
type = TransactionCardType.ReadOnly(),
|
||||
amountEquivalent = null,
|
||||
tokenIconUrl = initialCurrencyTo?.iconUrl,
|
||||
tokenCurrency = initialCurrencyTo?.symbol ?: "",
|
||||
tokenCurrency = initialCurrencyTo?.symbol.orEmpty(),
|
||||
token = null,
|
||||
amountTextFieldValue = null,
|
||||
canSelectAnotherToken = false,
|
||||
|
|
@ -122,7 +122,7 @@ internal class StateBuilder(
|
|||
fee = FeeItemState.Empty,
|
||||
swapButton = SwapButton(
|
||||
walletInteractionIcon = walletInterationIcon(userWalletProvider()),
|
||||
enabled = false,
|
||||
isEnabled = false,
|
||||
onClick = {},
|
||||
),
|
||||
onRefresh = {},
|
||||
|
|
@ -181,7 +181,7 @@ internal class StateBuilder(
|
|||
fee = FeeItemState.Empty,
|
||||
swapButton = SwapButton(
|
||||
walletInteractionIcon = walletInterationIcon(userWalletProvider()),
|
||||
enabled = false,
|
||||
isEnabled = false,
|
||||
onClick = { },
|
||||
),
|
||||
changeCardsButtonState = ChangeCardsButtonState.DISABLED,
|
||||
|
|
@ -246,7 +246,7 @@ internal class StateBuilder(
|
|||
fee = FeeItemState.Empty,
|
||||
swapButton = SwapButton(
|
||||
walletInteractionIcon = walletInterationIcon(userWalletProvider()),
|
||||
enabled = false,
|
||||
isEnabled = false,
|
||||
onClick = {},
|
||||
),
|
||||
providerState = ProviderState.Loading(),
|
||||
|
|
@ -335,7 +335,7 @@ internal class StateBuilder(
|
|||
),
|
||||
receiveCardData = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.ReadOnly(
|
||||
showWarning = true,
|
||||
shouldShowWarning = true,
|
||||
onWarningClick = actions.onReceiveCardWarningClick,
|
||||
accountTitleUM = getToCardAccountTitle(toAccount),
|
||||
),
|
||||
|
|
@ -368,7 +368,7 @@ internal class StateBuilder(
|
|||
fee = feeState,
|
||||
swapButton = SwapButton(
|
||||
walletInteractionIcon = walletInterationIcon(userWalletProvider()),
|
||||
enabled = getSwapButtonEnabled(notifications),
|
||||
isEnabled = getSwapButtonEnabled(notifications),
|
||||
onClick = actions.onSwapClick,
|
||||
),
|
||||
changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible),
|
||||
|
|
@ -397,17 +397,17 @@ internal class StateBuilder(
|
|||
|
||||
private fun createTosState(swapProvider: SwapProvider): TosState {
|
||||
return TosState(
|
||||
tosLink = swapProvider.termsOfUse?.let {
|
||||
tosLink = swapProvider.termsOfUse?.let { termsUrl ->
|
||||
LegalState(
|
||||
title = resourceReference(R.string.common_terms_of_use),
|
||||
link = it,
|
||||
link = termsUrl,
|
||||
onClick = actions.onLinkClick,
|
||||
)
|
||||
},
|
||||
policyLink = swapProvider.privacyPolicy?.let {
|
||||
policyLink = swapProvider.privacyPolicy?.let { policyUrl ->
|
||||
LegalState(
|
||||
title = resourceReference(R.string.common_privacy_policy),
|
||||
link = it,
|
||||
link = policyUrl,
|
||||
onClick = actions.onLinkClick,
|
||||
)
|
||||
},
|
||||
|
|
@ -420,12 +420,13 @@ internal class StateBuilder(
|
|||
}
|
||||
|
||||
private fun getSwapButtonEnabled(notifications: ImmutableList<NotificationUM>): Boolean {
|
||||
return notifications.none {
|
||||
it is SwapNotificationUM.Error || it is NotificationUM.Error ||
|
||||
it is SwapNotificationUM.Warning.ExpressError || it is SwapNotificationUM.Warning.ExpressGeneralError ||
|
||||
it is SwapNotificationUM.Warning.NoAvailableTokensToSwap ||
|
||||
it is SwapNotificationUM.Warning.NeedReserveToCreateAccount ||
|
||||
it is SwapNotificationUM.Info.PermissionNeeded
|
||||
return notifications.none { notification ->
|
||||
notification is SwapNotificationUM.Error || notification is NotificationUM.Error ||
|
||||
notification is SwapNotificationUM.Warning.ExpressError ||
|
||||
notification is SwapNotificationUM.Warning.ExpressGeneralError ||
|
||||
notification is SwapNotificationUM.Warning.NoAvailableTokensToSwap ||
|
||||
notification is SwapNotificationUM.Warning.NeedReserveToCreateAccount ||
|
||||
notification is SwapNotificationUM.Info.PermissionNeeded
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -495,7 +496,7 @@ internal class StateBuilder(
|
|||
fee = FeeItemState.Empty,
|
||||
swapButton = SwapButton(
|
||||
walletInteractionIcon = walletInterationIcon(userWalletProvider()),
|
||||
enabled = false,
|
||||
isEnabled = false,
|
||||
onClick = actions.onSwapClick,
|
||||
),
|
||||
changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible),
|
||||
|
|
@ -592,7 +593,7 @@ internal class StateBuilder(
|
|||
fee = FeeItemState.Empty,
|
||||
swapButton = SwapButton(
|
||||
walletInteractionIcon = walletInterationIcon(userWalletProvider()),
|
||||
enabled = false,
|
||||
isEnabled = false,
|
||||
onClick = { },
|
||||
),
|
||||
changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible),
|
||||
|
|
@ -604,7 +605,7 @@ internal class StateBuilder(
|
|||
fun createSwapInProgressState(uiState: SwapStateHolder): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
swapButton = uiState.swapButton.copy(
|
||||
enabled = false,
|
||||
isEnabled = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -722,15 +723,17 @@ internal class StateBuilder(
|
|||
)
|
||||
val selectTokenState = uiState.selectTokenState?.copy(
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
availableTokens = uiState.selectTokenState.availableTokens.map {
|
||||
when (it) {
|
||||
availableTokens = uiState.selectTokenState.availableTokens.map { tokenState ->
|
||||
when (tokenState) {
|
||||
is TokenToSelectState.TokenToSelect -> {
|
||||
it.copy(
|
||||
addedTokenBalanceData = it.addedTokenBalanceData?.copy(isBalanceHidden = isBalanceHidden),
|
||||
tokenState.copy(
|
||||
addedTokenBalanceData = tokenState.addedTokenBalanceData?.copy(
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
),
|
||||
)
|
||||
}
|
||||
is TokenToSelectState.Title -> {
|
||||
it
|
||||
tokenState
|
||||
}
|
||||
}
|
||||
}.toImmutableList(),
|
||||
|
|
@ -804,7 +807,7 @@ internal class StateBuilder(
|
|||
fun loadingPermissionState(uiState: SwapStateHolder): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
swapButton = uiState.swapButton.copy(
|
||||
enabled = false,
|
||||
isEnabled = false,
|
||||
),
|
||||
permissionState = GiveTxPermissionState.InProgress,
|
||||
notifications = notificationsFactory.getApprovalInProgressStateNotification(uiState.notifications),
|
||||
|
|
@ -837,7 +840,7 @@ internal class StateBuilder(
|
|||
txUrl = txUrl,
|
||||
providerName = stringReference(providerState.name),
|
||||
providerType = stringReference(providerState.type),
|
||||
showStatusButton = shouldShowStatus,
|
||||
shouldShowStatusButton = shouldShowStatus,
|
||||
providerIcon = providerState.iconUrl,
|
||||
rate = providerState.subtitle,
|
||||
fee = stringReference("${fee.feeCryptoFormattedWithNative} (${fee.feeFiatFormattedWithNative})"),
|
||||
|
|
@ -877,7 +880,7 @@ internal class StateBuilder(
|
|||
txUrl = txUrl,
|
||||
providerName = stringReference(providerState.name),
|
||||
providerType = stringReference(providerState.type),
|
||||
showStatusButton = false,
|
||||
shouldShowStatusButton = false,
|
||||
providerIcon = providerState.iconUrl,
|
||||
rate = providerState.subtitle,
|
||||
fee = TextReference.EMPTY,
|
||||
|
|
@ -1101,8 +1104,8 @@ internal class StateBuilder(
|
|||
onDismiss: () -> Unit,
|
||||
): SwapStateHolder {
|
||||
val availableProvidersStates = providersStates.entries
|
||||
.mapNotNull {
|
||||
it.convertToProviderBottomSheetState(
|
||||
.mapNotNull { entry ->
|
||||
entry.convertToProviderBottomSheetState(
|
||||
pricesLowerBest = pricesLowerBest,
|
||||
onProviderSelect = actions.onProviderSelect,
|
||||
needApplyFCARestrictions = needApplyFCARestrictions,
|
||||
|
|
@ -1139,19 +1142,19 @@ internal class StateBuilder(
|
|||
uiState.copy(
|
||||
bottomSheetConfig = uiState.bottomSheetConfig.copy(
|
||||
content = config.copy(
|
||||
providers = providers.map {
|
||||
val tokenInfo = tokenSwapInfoForProviders[it.id]
|
||||
if (it is ProviderState.Content && tokenInfo != null) {
|
||||
providers = providers.map { providerState ->
|
||||
val tokenInfo = tokenSwapInfoForProviders[providerState.id]
|
||||
if (providerState is ProviderState.Content && tokenInfo != null) {
|
||||
val rateString = tokenInfo.tokenAmount
|
||||
.getFormattedCryptoAmount(tokenInfo.cryptoCurrencyStatus.currency)
|
||||
it.copy(
|
||||
providerState.copy(
|
||||
subtitle = stringReference(rateString),
|
||||
percentLowerThenBest = pricesLowerBest[it.id]?.let { percent ->
|
||||
percentLowerThenBest = pricesLowerBest[providerState.id]?.let { percent ->
|
||||
PercentDifference.Value(percent)
|
||||
} ?: PercentDifference.Value(0f),
|
||||
)
|
||||
} else {
|
||||
it
|
||||
providerState
|
||||
}
|
||||
}.toImmutableList(),
|
||||
),
|
||||
|
|
@ -1170,8 +1173,8 @@ internal class StateBuilder(
|
|||
): SwapStateHolder {
|
||||
val config = ChooseFeeBottomSheetConfig(
|
||||
selectedFee = selectedFee,
|
||||
onSelectFeeType = {
|
||||
val selectedItem = when (it) {
|
||||
onSelectFeeType = { feeType ->
|
||||
val selectedItem = when (feeType) {
|
||||
FeeType.NORMAL -> txFeeState.normalFee
|
||||
FeeType.PRIORITY -> txFeeState.priorityFee
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ private fun TransactionCardData(
|
|||
balance = swapCardState.balance.orMaskWithStars(swapCardState.isBalanceHidden),
|
||||
textFieldValue = swapCardState.amountTextFieldValue,
|
||||
amountEquivalent = swapCardState.amountEquivalent,
|
||||
tokenIconUrl = swapCardState.tokenIconUrl ?: "",
|
||||
tokenIconUrl = swapCardState.tokenIconUrl.orEmpty(),
|
||||
tokenCurrency = swapCardState.tokenCurrency,
|
||||
priceImpact = priceImpact,
|
||||
networkIconRes = if (swapCardState.isNotNativeToken) swapCardState.networkIconRes else null,
|
||||
|
|
@ -374,7 +374,7 @@ private fun MainButton(state: SwapStateHolder) {
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResourceSafe(id = R.string.swapping_swap_action),
|
||||
iconResId = state.swapButton.walletInteractionIcon,
|
||||
enabled = state.swapButton.enabled,
|
||||
enabled = state.swapButton.isEnabled,
|
||||
onClick = state.swapButton.onClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -437,7 +437,7 @@ private val state = SwapStateHolder(
|
|||
),
|
||||
SwapNotificationUM.Warning.NoAvailableTokensToSwap("POLYGON"),
|
||||
),
|
||||
swapButton = SwapButton(enabled = true, onClick = {}, walletInteractionIcon = null),
|
||||
swapButton = SwapButton(isEnabled = true, onClick = {}, walletInteractionIcon = null),
|
||||
onRefresh = {},
|
||||
onBackClicked = {},
|
||||
onChangeCardsClicked = {},
|
||||
|
|
|
|||
|
|
@ -57,11 +57,11 @@ internal fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: ()
|
|||
val modifier = Modifier.padding(padding)
|
||||
when {
|
||||
state.availableTokens.isEmpty() && state.unavailableTokens.isEmpty() &&
|
||||
state.tokensListData.tokensList.isEmpty() && state.afterSearch -> {
|
||||
state.tokensListData.tokensList.isEmpty() && state.isAfterSearch -> {
|
||||
TokensNotFound(modifier)
|
||||
}
|
||||
state.availableTokens.isEmpty() && state.unavailableTokens.isEmpty() &&
|
||||
state.tokensListData.tokensList.isEmpty() && !state.afterSearch -> {
|
||||
state.tokensListData.tokensList.isEmpty() && !state.isAfterSearch -> {
|
||||
EmptyTokensList(modifier)
|
||||
}
|
||||
else -> {
|
||||
|
|
@ -313,7 +313,7 @@ private fun TokenItem(
|
|||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size72)
|
||||
.clickable(
|
||||
enabled = token.available,
|
||||
enabled = token.isAvailable,
|
||||
onClick = onTokenClick,
|
||||
)
|
||||
.padding(
|
||||
|
|
@ -336,7 +336,7 @@ private fun TokenItem(
|
|||
EllipsisText(
|
||||
text = token.name,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = if (token.available) {
|
||||
color = if (token.isAvailable) {
|
||||
TangemTheme.colors.text.primary1
|
||||
} else {
|
||||
TangemTheme.colors.text.tertiary
|
||||
|
|
@ -366,7 +366,7 @@ private fun TokenItem(
|
|||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Visible,
|
||||
color = if (token.available) {
|
||||
color = if (token.isAvailable) {
|
||||
TangemTheme.colors.text.primary1
|
||||
} else {
|
||||
TangemTheme.colors.text.tertiary
|
||||
|
|
@ -417,7 +417,7 @@ private fun TokenScreenPreview() {
|
|||
availableTokens = listOf(title, token, token, token).toImmutableList(),
|
||||
unavailableTokens = listOf(title, token, token, token).toImmutableList(),
|
||||
tokensListData = TokenListUMData.EmptyList,
|
||||
afterSearch = false,
|
||||
isAfterSearch = false,
|
||||
isBalanceHidden = false,
|
||||
onSearchEntered = {},
|
||||
onTokenSelected = {},
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ fun SwapSuccessScreen(state: SwapSuccessStateHolder, onBack: () -> Unit) {
|
|||
SwapSuccessScreenButtons(
|
||||
textRes = R.string.common_close,
|
||||
txUrl = state.txUrl,
|
||||
showStatusButton = state.showStatusButton,
|
||||
shouldShowStatusButton = state.shouldShowStatusButton,
|
||||
onExploreClick = state.onExploreButtonClick,
|
||||
onStatusClick = state.onStatusButtonClick,
|
||||
onDoneClick = onBack,
|
||||
|
|
@ -160,7 +160,7 @@ private fun SwapAmountBlock(
|
|||
private fun SwapSuccessScreenButtons(
|
||||
@StringRes textRes: Int,
|
||||
txUrl: String,
|
||||
showStatusButton: Boolean,
|
||||
shouldShowStatusButton: Boolean,
|
||||
onExploreClick: () -> Unit,
|
||||
onStatusClick: () -> Unit,
|
||||
onDoneClick: () -> Unit,
|
||||
|
|
@ -178,7 +178,7 @@ private fun SwapSuccessScreenButtons(
|
|||
onClick = onExploreClick,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (showStatusButton) {
|
||||
if (shouldShowStatusButton) {
|
||||
SpacerW12()
|
||||
SecondaryButtonIconStart(
|
||||
text = stringResourceSafe(id = R.string.express_cex_status_button_title),
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.SwapTokenScreenTestTags
|
||||
import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.feature.swap.domain.models.ui.PriceImpact
|
||||
import com.tangem.feature.swap.models.TransactionCardType
|
||||
|
|
@ -217,9 +216,9 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie
|
|||
}
|
||||
SpacerW16()
|
||||
if (balance.isNotBlank()) {
|
||||
AnimatedContent(targetState = balance, label = "") {
|
||||
AnimatedContent(targetState = balance, label = "") { balanceText ->
|
||||
Text(
|
||||
text = it,
|
||||
text = balanceText,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
|
|
@ -318,15 +317,15 @@ private fun Content(
|
|||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
} else {
|
||||
AnimatedContent(targetState = amountEquivalent, label = "") {
|
||||
AnimatedContent(targetState = amountEquivalent, label = "") { amount ->
|
||||
Text(
|
||||
text = it,
|
||||
text = amount,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (type.showWarning) {
|
||||
if (type.shouldShowWarning) {
|
||||
SpacerW4()
|
||||
IconButton(
|
||||
onClick = {
|
||||
|
|
@ -348,9 +347,9 @@ private fun Content(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
AnimatedContent(targetState = amountEquivalent, label = "") {
|
||||
AnimatedContent(targetState = amountEquivalent, label = "") { amount ->
|
||||
Text(
|
||||
text = it,
|
||||
text = amount,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20),
|
||||
|
|
@ -409,6 +408,7 @@ fun Token(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("NullableToStringCall")
|
||||
@Composable
|
||||
private fun TokenIcon(
|
||||
tokenIconUrl: String,
|
||||
|
|
@ -595,7 +595,7 @@ private fun TransactionCardPreview() {
|
|||
private fun TransactionCardPreviewWithPriceImpact() {
|
||||
TransactionCard(
|
||||
type = TransactionCardType.ReadOnly(
|
||||
showWarning = true,
|
||||
shouldShowWarning = true,
|
||||
accountTitleUM = AccountTitleUM.Account(
|
||||
prefixText = resourceReference(R.string.common_from),
|
||||
name = AccountNameUM.DefaultMain.value,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue