Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-12 15:11:58 +05:00
parent eb73a64054
commit 00d55ff5a6
39 changed files with 195 additions and 272 deletions

View file

@ -1,46 +0,0 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:ConfirmUM.kt$ConfirmUM.Content$val showTapHelp: Boolean</ID>
<ID>BooleanPropertyNaming:SwapAmountModel.kt$SwapAmountModel$private var showBestRateAnimation: Boolean = false</ID>
<ID>BooleanPropertyNaming:SwapAmountModel.kt$SwapAmountModel$val showSendViaSwapNotification = shouldShowNotificationUseCase( NotificationId.SendViaSwapTokenSelectorNotification.key, )</ID>
<ID>BooleanPropertyNaming:SwapAmountPrimaryReadyStateTransformer.kt$SwapAmountPrimaryReadyStateTransformer$private val showBestRateAnimation: Boolean</ID>
<ID>BooleanPropertyNaming:SwapAmountSecondaryReadyStateTransformer.kt$SwapAmountSecondaryReadyStateTransformer$private val showBestRateAnimation: Boolean</ID>
<ID>BooleanPropertyNaming:SwapAmountSelectQuoteTransformer.kt$SwapAmountSelectQuoteTransformer$private val needApplyFCARestrictions: Boolean</ID>
<ID>BooleanPropertyNaming:SwapAmountSetQuotesTransformer.kt$SwapAmountSetQuotesTransformer$private val needApplyFcaRestrictions: Boolean</ID>
<ID>BooleanPropertyNaming:SwapAmountUM.kt$SwapAmountUM.Content$val showBestRateAnimation: Boolean</ID>
<ID>BooleanPropertyNaming:SwapAmountUM.kt$SwapAmountUM.Content$val showFCAWarning: Boolean</ID>
<ID>BooleanPropertyNaming:SwapChooseProviderModel.kt$SwapChooseProviderModel$private val needApplyFCARestrictions = params.userCountry.needApplyFCARestrictions()</ID>
<ID>BooleanPropertyNaming:SwapProviderListItemConverter.kt$SwapProviderListItemConverter$private val needApplyFCARestrictions: Boolean</ID>
<ID>BooleanPropertyNaming:SwapProviderStateConverter.kt$SwapProviderStateConverter$private val needApplyFCARestrictions: Boolean</ID>
<ID>CouldBeSequence:SwapAmountModel.kt$SwapAmountModel$filter { it.currencyStatus.currency.id == toCryptoCurrency.id }</ID>
<ID>MaxChainedCallsOnSameLine:SwapAlertFactory.kt$SwapAlertFactory$confirmData?.fee?.amount?.value?.toString().orEmpty()</ID>
<ID>MultilineLambdaItParameter:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel${ it.copy( confirmUM = SendWithSwapConfirmInitialStateTransformer( isShowTapHelp = isShowTapHelp, ).transform(uiState.value.confirmUM), ) }</ID>
<ID>MultilineLambdaItParameter:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel${ val confirmUM = it.confirmUM as? ConfirmUM.Content it.copy(confirmUM = confirmUM?.copy(showTapHelp = showTapHelp) ?: it.confirmUM) }</ID>
<ID>MultilineLambdaItParameter:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel${ val feeUM = it.feeSelectorUM as? FeeSelectorUM.Content it.copy( confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( isPrimaryButtonEnabled = !hasError &amp;&amp; feeUM != null, ) ?: it.confirmUM, ) }</ID>
<ID>MultilineLambdaItParameter:SendWithSwapConfirmationNotificationsTransformer.kt$SendWithSwapConfirmationNotificationsTransformer${ ConfirmUM.Content.LegalUM( title = resourceReference(R.string.common_privacy_policy), link = it, ) }</ID>
<ID>MultilineLambdaItParameter:SendWithSwapConfirmationNotificationsTransformer.kt$SendWithSwapConfirmationNotificationsTransformer${ ConfirmUM.Content.LegalUM( title = resourceReference(R.string.common_terms_of_use), link = it, ) }</ID>
<ID>MultilineLambdaItParameter:SendWithSwapModel.kt$SendWithSwapModel${ Timber.w(it.toString()) swapAlertFactory.getGenericErrorState( expressError = ExpressError.UnknownError, onFailedTxEmailClick = { modelScope.launch { swapAlertFactory.onFailedTxEmailClick( userWallet = userWallet, cryptoCurrency = params.currency, errorMessage = it.toString(), ) } }, popBack = ::onBackClick, ) }</ID>
<ID>MultilineLambdaItParameter:SendWithSwapModel.kt$SendWithSwapModel${ it.copy( destinationUM = DestinationUM.Empty(), feeSelectorUM = FeeSelectorUM.Loading, confirmUM = ConfirmUM.Empty, navigationUM = NavigationUM.Empty, ) }</ID>
<ID>MultilineLambdaItParameter:SwapAmountSetQuotesTransformer.kt$SwapAmountSetQuotesTransformer${ it is SwapQuoteUM.Content || it is SwapQuoteUM.Allowance || (it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError }</ID>
<ID>MultilineLambdaItParameter:SwapChooseProviderModel.kt$SwapChooseProviderModel${ it is SwapQuoteUM.Content || it is SwapQuoteUM.Allowance || (it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError }</ID>
<ID>MultilineLambdaItParameter:SwapChooseTokenNetworkModel.kt$SwapChooseTokenNetworkModel${ Timber.e("Failed to get user wallet: $it") swapChooseTokenAlertFactory.getGenericErrorState(params.onDismiss) return }</ID>
<ID>MultilineLambdaItParameter:SwapChooseTokenNetworkModel.kt$SwapChooseTokenNetworkModel${ Timber.e(it.toString()) uiState.update( SwapChooseErrorStateTransformer( tokenName = params.token.name, onDismiss = params.onDismiss, ), ) return@launch }</ID>
<ID>MultilineLambdaItParameter:SwapTransactionSender.kt$SwapTransactionSender${ Timber.e(it, "Failed to create swap CEX tx data") onSendError(SendTransactionError.UnknownError(Exception(it))) return }</ID>
<ID>MultilineLambdaItParameter:SwapTransactionSender.kt$SwapTransactionSender${ onExpressError(it); return }</ID>
<ID>NamedArguments:SwapTransactionSender.kt$SwapTransactionSender$getSwapDataUseCase( userWallet = userWallet, fromCryptoCurrencyStatus = fromStatus, fromAmount = fromAmount.toStringWithRightOffset(fromStatus.currency.decimals), toCryptoCurrency = toStatus.currency, toAddress = destination, expressProvider = provider, rateType = rateType, expressOperationType, )</ID>
<ID>NoNameShadowing:SendWithSwapContent.kt$navigationUM</ID>
<ID>NoNameShadowing:SwapAmountContent.kt$amountFieldUM</ID>
<ID>NullableToStringCall:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel$error.toString()</ID>
<ID>NullableToStringCall:SwapAmountModel.kt$SwapAmountModel$$primaryStatus</ID>
<ID>NullableToStringCall:SwapAmountModel.kt$SwapAmountModel$$secondaryStatus</ID>
<ID>PropertyUsedBeforeDeclaration:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel$amountUM</ID>
<ID>UnnecessaryEventHandlerParameter:SwapAmountContent.kt$onExpandEditField: (SwapAmountType) -&gt; Unit</ID>
<ID>UseEmptyCounterpart:SendWithSwapAnalyticEvents.kt$SendWithSwapAnalyticEvents$mapOf()</ID>
<ID>UseEmptyCounterpart:SendWithSwapSuccessContent.kt$listOf()</ID>
<ID>UseEmptyCounterpart:SwapAmountAnalyticEvents.kt$SwapAmountAnalyticEvents$mapOf()</ID>
<ID>VarCouldBeVal:SwapAmountModel.kt$SwapAmountModel$private var primaryCryptoCurrency: CryptoCurrency = params.primaryCryptoCurrencyStatusFlow.value.currency</ID>
<ID>VarCouldBeVal:SwapAmountModel.kt$SwapAmountModel$private var userWallet = params.userWallet</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -6,7 +6,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER
internal sealed class SwapAmountAnalyticEvents(
category: String,
event: String,
params: Map<String, String> = mapOf(),
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category = category, event = event, params = params) {
data class ProviderSelectorClicked(

View file

@ -48,11 +48,11 @@ internal sealed class SwapAmountUM {
val swapCurrencies: SwapCurrencies,
val swapQuotes: ImmutableList<SwapQuoteUM>,
val selectedQuote: SwapQuoteUM,
val showFCAWarning: Boolean,
val isShowFCAWarning: Boolean,
// extra data
val appCurrency: AppCurrency?,
val showBestRateAnimation: Boolean,
val isShowBestRateAnimation: Boolean,
) : SwapAmountUM()
}

View file

@ -26,11 +26,8 @@ import com.tangem.domain.notifications.ShouldShowNotificationUseCase
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
import com.tangem.domain.swap.models.SwapCurrencies
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.domain.swap.models.*
import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection
import com.tangem.domain.swap.models.SwapQuoteModel
import com.tangem.domain.swap.models.getGroupWithDirection
import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase
import com.tangem.domain.swap.usecase.SelectInitialPairUseCase
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
@ -99,9 +96,9 @@ internal class SwapAmountModel @Inject constructor(
private val params: SwapAmountComponentParams = paramsContainer.require()
private val swapDirection = params.swapDirection
private var appCurrency = AppCurrency.Default
private var userWallet = params.userWallet
private val userWallet = params.userWallet
private var primaryCryptoCurrency: CryptoCurrency = params.primaryCryptoCurrencyStatusFlow.value.currency
private val primaryCryptoCurrency: CryptoCurrency = params.primaryCryptoCurrencyStatusFlow.value.currency
private var primaryMaximumAmountBoundary: EnterAmountBoundary by Delegates.notNull()
private var primaryMinimumAmountBoundary: EnterAmountBoundary by Delegates.notNull()
@ -111,7 +108,7 @@ internal class SwapAmountModel @Inject constructor(
private var userCountry: UserCountry = UserCountry.Other(Locale.getDefault().country)
val bottomSheetNavigation: SlotNavigation<SwapChooseProviderConfig> = SlotNavigation()
private var showBestRateAnimation: Boolean = false
private var isShowBestRateAnimation: Boolean = false
val uiState: StateFlow<SwapAmountUM>
field = MutableStateFlow(params.amountUM)
@ -124,7 +121,7 @@ internal class SwapAmountModel @Inject constructor(
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
userCountry = getUserCountryUseCase.invokeSync().getOrNull()
?: UserCountry.Other(Locale.getDefault().country)
showBestRateAnimation = swapBestRateAnimationStore.getSyncOrNull()
isShowBestRateAnimation = swapBestRateAnimationStore.getSyncOrNull()
}
configAmountNavigation()
subscribeOnCryptoCurrencyStatusFlow()
@ -169,7 +166,7 @@ internal class SwapAmountModel @Inject constructor(
quoteUM = quoteUM,
secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary,
secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary,
needApplyFCARestrictions = userCountry.needApplyFCARestrictions(),
isNeedApplyFCARestrictions = userCountry.needApplyFCARestrictions(),
),
)
}
@ -270,7 +267,7 @@ internal class SwapAmountModel @Inject constructor(
override fun onSelectTokenClick() {
val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return
modelScope.launch {
val showSendViaSwapNotification = shouldShowNotificationUseCase(
val isShowSendViaSwapNotification = shouldShowNotificationUseCase(
NotificationId.SendViaSwapTokenSelectorNotification.key,
)
val isEditMode = amountParams.currentRoute.firstOrNull()?.isEditMode == true
@ -281,7 +278,7 @@ internal class SwapAmountModel @Inject constructor(
initialCurrency = primaryCryptoCurrency,
selectedCurrency = selectedCurrency.takeIf { isEditMode },
source = AppRoute.ChooseManagedTokens.Source.SendViaSwap,
shouldShowSendViaSwapNotification = showSendViaSwapNotification,
shouldShowSendViaSwapNotification = isShowSendViaSwapNotification,
analyticsCategoryName = params.analyticsCategoryName,
),
)
@ -306,7 +303,7 @@ internal class SwapAmountModel @Inject constructor(
fun onFinishAnimation() {
uiState.update {
(it as? SwapAmountUM.Content)?.copy(showBestRateAnimation = false) ?: it
(it as? SwapAmountUM.Content)?.copy(isShowBestRateAnimation = false) ?: it
}
}
@ -367,7 +364,7 @@ internal class SwapAmountModel @Inject constructor(
swapDirection = swapDirection,
clickIntents = this,
isBalanceHidden = params.isBalanceHidingFlow.value,
showBestRateAnimation = showBestRateAnimation,
isShowBestRateAnimation = isShowBestRateAnimation,
isSingleWallet = isOnlyOneWallet,
isAccountsMode = params.isAccountModeFlow.value,
account = params.accountFlow.value,
@ -418,7 +415,7 @@ internal class SwapAmountModel @Inject constructor(
swapDirection = swapDirection,
clickIntents = this,
isBalanceHidden = params.isBalanceHidingFlow.value,
showBestRateAnimation = showBestRateAnimation,
isShowBestRateAnimation = isShowBestRateAnimation,
isSingleWallet = isOnlyOneWallet,
isAccountsMode = isAccountsMode,
account = account,
@ -532,7 +529,7 @@ internal class SwapAmountModel @Inject constructor(
swapDirection = swapDirection,
clickIntents = this@SwapAmountModel,
isBalanceHidden = params.isBalanceHidingFlow.value,
showBestRateAnimation = showBestRateAnimation,
isShowBestRateAnimation = isShowBestRateAnimation,
isSingleWallet = isOnlyOneWallet,
isAccountsMode = params.isAccountModeFlow.value,
account = params.accountFlow.value,
@ -540,6 +537,7 @@ internal class SwapAmountModel @Inject constructor(
)
startLoadingQuotesTask(isSilentReload = false)
} else {
@Suppress("NullableToStringCall")
Timber.e(
"""
Invalid cryptocurrencies status:
@ -596,50 +594,50 @@ internal class SwapAmountModel @Inject constructor(
val isAmountScreen = params is SwapAmountComponentParams.AmountParams
val isAmountError = fromAmount?.amountTextField?.isError == true || fromAmountValue.isNullOrZero()
if (isAmountScreen && isAmountError) {
uiState.transformerUpdate(SwapQuoteEmptyStateTransformer)
return
uiState.transformerUpdate(SwapQuoteEmptyStateTransformer); return
}
if (!isSilentReload) uiState.transformerUpdate(SwapQuoteLoadingStateTransformer)
modelScope.launch {
val quotes = state.swapCurrencies.getGroupWithDirection(state.swapDirection).available.filter {
it.currencyStatus.currency.id == toCryptoCurrency.id
}.flatMap {
it.providers
}.map { provider ->
async {
getSwapQuoteUseCase(
userWallet = userWallet,
fromCryptoCurrency = fromCryptoCurrency,
toCryptoCurrency = toCryptoCurrency,
fromAmount = fromAmountValue,
provider = provider,
).fold(
ifLeft = { error ->
SwapQuoteUM.Error(
provider = provider,
expressError = error,
)
},
ifRight = { quote: SwapQuoteModel ->
SwapQuoteUMConverter(
primaryCurrency = fromCryptoCurrency,
secondaryCurrency = toCryptoCurrency,
swapDirection = swapDirection,
allowanceContract = quote.allowanceContract,
isApprovalNeeded = checkAllowance(state, quote),
fromAmount = fromAmountValue,
).convert(
SwapQuoteUMConverter.Data(
quote = quote,
val quotes = state.swapCurrencies.getGroupWithDirection(state.swapDirection).available
.asSequence()
.filter { swapCurrencyStatus -> swapCurrencyStatus.currencyStatus.currency.id == toCryptoCurrency.id }
.flatMap(SwapCryptoCurrency::providers)
.toList()
.map { provider ->
async {
getSwapQuoteUseCase(
userWallet = userWallet,
fromCryptoCurrency = fromCryptoCurrency,
toCryptoCurrency = toCryptoCurrency,
fromAmount = fromAmountValue,
provider = provider,
).fold(
ifLeft = { error ->
SwapQuoteUM.Error(
provider = provider,
),
)
},
)
}
}.awaitAll()
expressError = error,
)
},
ifRight = { quote: SwapQuoteModel ->
SwapQuoteUMConverter(
primaryCurrency = fromCryptoCurrency,
secondaryCurrency = toCryptoCurrency,
swapDirection = swapDirection,
allowanceContract = quote.allowanceContract,
isApprovalNeeded = checkAllowance(state, quote),
fromAmount = fromAmountValue,
).convert(
SwapQuoteUMConverter.Data(
quote = quote,
provider = provider,
),
)
},
)
}
}.awaitAll()
uiState.transformerUpdate(
SwapAmountSetQuotesTransformer(
@ -647,7 +645,7 @@ internal class SwapAmountModel @Inject constructor(
secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary,
secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary,
isSilentReload = isSilentReload,
needApplyFcaRestrictions = userCountry.needApplyFCARestrictions(),
isNeedApplyFcaRestrictions = userCountry.needApplyFCARestrictions(),
),
)
feeSelectorReloadTrigger.triggerUpdate()

View file

@ -24,7 +24,7 @@ internal class SwapAmountPrimaryReadyStateTransformer(
private val clickIntents: AmountScreenClickIntents,
private val swapDirection: SwapDirection,
private val isBalanceHidden: Boolean,
private val showBestRateAnimation: Boolean,
private val isShowBestRateAnimation: Boolean,
private val isSingleWallet: Boolean,
private val isAccountsMode: Boolean,
private val account: Account.CryptoPortfolio?,
@ -60,8 +60,8 @@ internal class SwapAmountPrimaryReadyStateTransformer(
swapQuotes = persistentListOf(),
selectedQuote = SwapQuoteUM.Empty,
appCurrency = appCurrency,
showBestRateAnimation = showBestRateAnimation,
showFCAWarning = false,
isShowBestRateAnimation = isShowBestRateAnimation,
isShowFCAWarning = false,
)
}
}

View file

@ -25,7 +25,7 @@ internal class SwapAmountSecondaryReadyStateTransformer(
private val clickIntents: AmountScreenClickIntents,
private val swapDirection: SwapDirection,
private val isBalanceHidden: Boolean,
private val showBestRateAnimation: Boolean,
private val isShowBestRateAnimation: Boolean,
private val isSingleWallet: Boolean,
private val isAccountsMode: Boolean,
private val account: Account.CryptoPortfolio?,
@ -59,8 +59,8 @@ internal class SwapAmountSecondaryReadyStateTransformer(
swapQuotes = persistentListOf(),
selectedQuote = SwapQuoteUM.Empty,
appCurrency = appCurrency,
showBestRateAnimation = showBestRateAnimation,
showFCAWarning = false,
isShowBestRateAnimation = isShowBestRateAnimation,
isShowFCAWarning = false,
)
}
}

View file

@ -19,7 +19,7 @@ internal class SwapAmountSelectQuoteTransformer(
private val quoteUM: SwapQuoteUM,
private val secondaryMaximumAmountBoundary: EnterAmountBoundary?,
private val secondaryMinimumAmountBoundary: EnterAmountBoundary?,
private val needApplyFCARestrictions: Boolean,
private val isNeedApplyFCARestrictions: Boolean,
) : Transformer<SwapAmountUM> {
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
if (prevState !is SwapAmountUM.Content) return prevState
@ -31,7 +31,7 @@ internal class SwapAmountSelectQuoteTransformer(
return prevState.copy(
isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content,
selectedQuote = quoteUM,
showFCAWarning = needApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true,
isShowFCAWarning = isNeedApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true,
primaryAmount = if (prevState.selectedAmountType == SwapAmountType.From) {
val swapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content
val amountField = swapAmountField?.amountField as? AmountState.Data

View file

@ -22,14 +22,14 @@ internal class SwapAmountSetQuotesTransformer(
private val secondaryMaximumAmountBoundary: EnterAmountBoundary?,
private val secondaryMinimumAmountBoundary: EnterAmountBoundary?,
private val isSilentReload: Boolean,
private val needApplyFcaRestrictions: Boolean,
private val isNeedApplyFcaRestrictions: Boolean,
) : Transformer<SwapAmountUM> {
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
if (prevState !is SwapAmountUM.Content) return prevState
val isSingleProvider = quotes.filter {
it is SwapQuoteUM.Content || it is SwapQuoteUM.Allowance ||
(it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError
val isSingleProvider = quotes.filter { swapQuoteUM ->
swapQuoteUM is SwapQuoteUM.Content || swapQuoteUM is SwapQuoteUM.Allowance ||
(swapQuoteUM as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError
}.isSingleItem()
val sortedQuotes = quotes.sortedWith(SwapQuotesComparator)
@ -49,7 +49,7 @@ internal class SwapAmountSetQuotesTransformer(
quoteUM = selectedQuote,
secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary,
secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary,
needApplyFCARestrictions = needApplyFcaRestrictions &&
isNeedApplyFCARestrictions = isNeedApplyFcaRestrictions &&
selectedQuote.provider?.isRestrictedByFCA() == true,
)

View file

@ -88,7 +88,7 @@ internal fun SwapAmountBlockContent(
SwapChooseProviderContent(
isBestRate = isBestRate,
isSingleProvider = quoteContent?.isSingleProvider == true,
showBestRateAnimation = amountUM.showBestRateAnimation,
showBestRateAnimation = amountUM.isShowBestRateAnimation,
expressProvider = amountUM.selectedQuote.provider,
onClick = onProviderSelectClick,
onFinishAnimation = onFinishAnimation,
@ -98,7 +98,7 @@ internal fun SwapAmountBlockContent(
start.linkTo(parent.start)
end.linkTo(parent.end)
},
showFCAWarning = amountUM.showFCAWarning,
showFCAWarning = amountUM.isShowFCAWarning,
)
}
}

View file

@ -175,7 +175,7 @@ private fun SwapAmountBlock(
selectedQuote = selectedQuote,
isSelectedAmountType = isSelectedAmountType,
isFixedRate = isFixedRate,
onExpandEditField = clickIntents::onExpandEditField,
onExpandEditField = { clickIntents.onExpandEditField(amountFieldUM.amountType) },
onSelectTokenClick = clickIntents::onSelectTokenClick,
onMaxAmountClick = clickIntents::onMaxValueClick,
)
@ -195,14 +195,14 @@ private fun SwapAmountEditBlock(
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier.padding(top = 48.dp, bottom = 28.dp),
) {
when (val amountFieldUM = amountFieldUM.amountField) {
when (val amountField = amountFieldUM.amountField) {
!is AmountState.Data -> {
TextShimmer(
style = TangemTheme.typography.caption2,
modifier = Modifier.width(60.dp),
)
}
else -> AccountTitle(amountFieldUM.accountTitleUM)
else -> AccountTitle(amountField.accountTitleUM)
}
AmountFieldV2(
amountUM = amountFieldUM.amountField,
@ -221,7 +221,7 @@ private fun SwapAmountInfo(
selectedQuote: SwapQuoteUM?,
isSelectedAmountType: Boolean,
isFixedRate: Boolean,
onExpandEditField: (SwapAmountType) -> Unit,
onExpandEditField: () -> Unit,
onMaxAmountClick: () -> Unit,
onSelectTokenClick: () -> Unit,
modifier: Modifier = Modifier,
@ -237,7 +237,7 @@ private fun SwapAmountInfo(
enabled = (amountFieldUM as? SwapAmountFieldUM.Content)?.isClickEnabled == true,
onClick = {
if (isFixedRate) {
onExpandEditField(amountFieldUM.amountType)
onExpandEditField()
} else {
onSelectTokenClick()
}

View file

@ -90,8 +90,8 @@ internal data object SwapAmountContentPreview {
secondaryCryptoCurrencyStatus = cryptoCurrencyStatus,
swapRateType = ExpressRateType.Float,
appCurrency = AppCurrency.Default,
showBestRateAnimation = false,
showFCAWarning = false,
isShowBestRateAnimation = false,
isShowFCAWarning = false,
)
val defaultState = SwapAmountUM.Content(
@ -129,8 +129,8 @@ internal data object SwapAmountContentPreview {
secondaryCryptoCurrencyStatus = cryptoCurrencyStatus,
swapRateType = ExpressRateType.Float,
isPrimaryButtonEnabled = true,
showBestRateAnimation = false,
showFCAWarning = true,
isShowBestRateAnimation = false,
isShowFCAWarning = true,
)
val defaultStateAccount: SwapAmountUM.Content

View file

@ -25,13 +25,13 @@ internal class SwapChooseProviderModel @Inject constructor(
private val params: SwapChooseProviderComponent.Params = paramsContainer.require()
private val needApplyFCARestrictions = params.userCountry.needApplyFCARestrictions()
private val isNeedApplyFCARestrictions = params.userCountry.needApplyFCARestrictions()
private val swapProviderListItemConverter by lazy(LazyThreadSafetyMode.NONE) {
SwapProviderListItemConverter(
cryptoCurrency = params.cryptoCurrency,
selectedProvider = params.selectedProvider,
needApplyFCARestrictions = needApplyFCARestrictions,
isNeedApplyFCARestrictions = isNeedApplyFCARestrictions,
needBestRateBadge = params.providers.filterIsInstance<SwapQuoteUM.Content>().isSingleItem().not(),
)
}
@ -45,13 +45,13 @@ internal class SwapChooseProviderModel @Inject constructor(
}
private fun getInitialState(): SwapChooseProviderBottomSheetContent {
val filteredProviderList = params.providers.filter {
it is SwapQuoteUM.Content ||
it is SwapQuoteUM.Allowance ||
(it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError
val filteredProviderList = params.providers.filter { swapQuoteUM ->
swapQuoteUM is SwapQuoteUM.Content ||
swapQuoteUM is SwapQuoteUM.Allowance ||
(swapQuoteUM as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError
}
return SwapChooseProviderBottomSheetContent(
isApplyFCARestrictions = needApplyFCARestrictions && params.selectedProvider.isRestrictedByFCA(),
isApplyFCARestrictions = isNeedApplyFCARestrictions && params.selectedProvider.isRestrictedByFCA(),
providerList = swapProviderListItemConverter.convertList(filteredProviderList)
.filterNotNull()
.toPersistentList(),

View file

@ -21,14 +21,14 @@ import com.tangem.utils.converter.Converter
internal class SwapProviderListItemConverter(
private val cryptoCurrency: CryptoCurrency,
private val selectedProvider: ExpressProvider,
private val needApplyFCARestrictions: Boolean,
private val isNeedApplyFCARestrictions: Boolean,
needBestRateBadge: Boolean,
) : Converter<SwapQuoteUM, SwapProviderListItem?> {
private val providerStateConverter = SwapProviderStateConverter(
cryptoCurrency = cryptoCurrency,
selectedProvider = selectedProvider,
needApplyFCARestrictions = needApplyFCARestrictions,
isNeedApplyFCARestrictions = isNeedApplyFCARestrictions,
isNeedBestRateBadge = needBestRateBadge,
)
@ -78,7 +78,7 @@ internal class SwapProviderListItemConverter(
},
)
}
is SwapQuoteUM.Content -> if (needApplyFCARestrictions && value.provider.isRestrictedByFCA()) {
is SwapQuoteUM.Content -> if (isNeedApplyFCARestrictions && value.provider.isRestrictedByFCA()) {
ProviderChooseUM.ExtraUM.Action(
text = resourceReference(R.string.express_provider_fca_warning_list),
)

View file

@ -20,7 +20,7 @@ internal class SwapProviderStateConverter(
private val cryptoCurrency: CryptoCurrency,
private val selectedProvider: ExpressProvider,
private val isNeedBestRateBadge: Boolean,
private val needApplyFCARestrictions: Boolean,
private val isNeedApplyFCARestrictions: Boolean,
) : Converter<SwapQuoteUM, SwapProviderState> {
override fun convert(value: SwapQuoteUM): SwapProviderState {
@ -41,7 +41,7 @@ internal class SwapProviderStateConverter(
}
val additionalBadge = when {
needApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList
isNeedApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList
isNeedBestRateBadge && isBestRate -> AdditionalBadge.BestTrade
else -> AdditionalBadge.Empty
}
@ -59,7 +59,7 @@ internal class SwapProviderStateConverter(
private fun SwapQuoteUM.Error.convertToErrorContent(): SwapProviderState {
val additionalBadge = when {
needApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList
isNeedApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList
else -> AdditionalBadge.Empty
}

View file

@ -66,8 +66,8 @@ internal class SwapChooseTokenNetworkModel @Inject constructor(
}
private fun initContent() {
val userWallet = getUserWalletUseCase(params.userWalletId).getOrElse {
Timber.e("Failed to get user wallet: $it")
val userWallet = getUserWalletUseCase(params.userWalletId).getOrElse { error ->
Timber.e("Failed to get user wallet: $error")
swapChooseTokenAlertFactory.getGenericErrorState(params.onDismiss)
return
}
@ -87,8 +87,8 @@ internal class SwapChooseTokenNetworkModel @Inject constructor(
cryptoCurrencyList = cryptoCurrencyList + params.initialCurrency,
filterProviderTypes = SEND_WITH_SWAP_PROVIDER_TYPES,
swapTxType = SwapTxType.SendWithSwap,
).getOrElse {
Timber.e(it.toString())
).getOrElse { error ->
Timber.e(error.toString())
uiState.update(
SwapChooseErrorStateTransformer(
tokenName = params.token.name,

View file

@ -91,7 +91,8 @@ internal class SwapAlertFactory @Inject constructor(
destinationAddress = confirmData?.enteredDestination.orEmpty(),
tokenSymbol = confirmData?.toCryptoCurrencyStatus?.currency?.symbol.orEmpty(),
amount = confirmData?.enteredAmount?.toString().orEmpty(),
fee = confirmData?.fee?.amount?.value?.toString().orEmpty(),
fee = confirmData?.fee?.amount?.value?.toString()
.orEmpty(),
),
)

View file

@ -15,7 +15,7 @@ internal sealed class ConfirmUM {
data class Content(
override val isPrimaryButtonEnabled: Boolean = false,
val isTransactionInProcess: Boolean,
val showTapHelp: Boolean,
val isShowTapHelp: Boolean,
val sendingFooter: TextReference,
val notifications: ImmutableList<NotificationUM>,
val tosUM: TosUM?,

View file

@ -13,7 +13,7 @@ import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
internal sealed class SendWithSwapAnalyticEvents(
event: String,
params: Map<String, String> = mapOf(),
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category = CommonSendAnalyticEvents.SEND_CATEGORY, event = event, params = params) {
data class TransactionScreenOpened(

View file

@ -94,11 +94,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
field = MutableStateFlow(params.sendWithSwapUM)
val primaryCurrencyStatus: CryptoCurrencyStatus = params.primaryCryptoCurrencyStatusFlow.value
val secondaryCurrencyStatus: CryptoCurrencyStatus? = amountUM?.secondaryCryptoCurrencyStatus
val primaryFeePaidCurrencyStatus: CryptoCurrencyStatus = params.primaryFeePaidCurrencyStatusFlow.value
val secondaryCurrency: CryptoCurrency = requireNotNull(amountUM?.secondaryCryptoCurrencyStatus?.currency) {
"Crypto currency must not be null"
}
private val swapTransactionSender = swapTransactionSenderFactory.create(params.userWallet)
@ -111,6 +107,11 @@ internal class SendWithSwapConfirmModel @Inject constructor(
private val feeSelectorUM
get() = uiState.value.feeSelectorUM as? FeeSelectorUM.Content
val secondaryCurrencyStatus: CryptoCurrencyStatus? = amountUM?.secondaryCryptoCurrencyStatus
val secondaryCurrency: CryptoCurrency = requireNotNull(amountUM?.secondaryCryptoCurrencyStatus?.currency) {
"Crypto currency must not be null"
}
val confirmData: ConfirmData
get() {
val amountUM = amountUM
@ -270,13 +271,13 @@ internal class SendWithSwapConfirmModel @Inject constructor(
uiState.transformerUpdate(SendWithSwapConfirmSendingStateTransformer(false))
swapAlertFactory.getSendTransactionErrorState(
error = error,
onFailedTxEmailClick = {
onFailedTxEmailClick = { _ ->
modelScope.launch {
swapAlertFactory.onFailedTxEmailClick(
userWallet = params.userWallet,
cryptoCurrency = confirmData.fromCryptoCurrencyStatus?.currency,
confirmData = confirmData,
errorMessage = error.toString(),
errorMessage = error?.toString().orEmpty(),
)
}
},
@ -320,8 +321,8 @@ internal class SendWithSwapConfirmModel @Inject constructor(
modelScope.launch {
val isShowTapHelp = isSendTapHelpEnabledUseCase.invokeSync().getOrElse { false }
if (confirmUM is ConfirmUM.Empty) {
uiState.update {
it.copy(
uiState.update { state ->
state.copy(
confirmUM = SendWithSwapConfirmInitialStateTransformer(
isShowTapHelp = isShowTapHelp,
).transform(uiState.value.confirmUM),
@ -335,9 +336,9 @@ internal class SendWithSwapConfirmModel @Inject constructor(
private fun subscribeOnTapHelpUpdates() {
isSendTapHelpEnabledUseCase().getOrNull()
?.onEach { showTapHelp ->
uiState.update {
val confirmUM = it.confirmUM as? ConfirmUM.Content
it.copy(confirmUM = confirmUM?.copy(showTapHelp = showTapHelp) ?: it.confirmUM)
uiState.update { state ->
val confirmUM = state.confirmUM as? ConfirmUM.Content
state.copy(confirmUM = confirmUM?.copy(isShowTapHelp = showTapHelp) ?: state.confirmUM)
}
}?.launchIn(modelScope)
}
@ -376,12 +377,12 @@ internal class SendWithSwapConfirmModel @Inject constructor(
flow2 = swapNotificationsUpdateListener.hasErrorFlow,
) { hasSendError, hasSwapError ->
val hasError = hasSendError || hasSwapError
uiState.update {
val feeUM = it.feeSelectorUM as? FeeSelectorUM.Content
it.copy(
confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy(
uiState.update { state ->
val feeUM = state.feeSelectorUM as? FeeSelectorUM.Content
state.copy(
confirmUM = (state.confirmUM as? ConfirmUM.Content)?.copy(
isPrimaryButtonEnabled = !hasError && feeUM != null,
) ?: it.confirmUM,
) ?: state.confirmUM,
)
}
}.launchIn(modelScope)

View file

@ -98,8 +98,8 @@ internal class SwapTransactionSender @AssistedInject constructor(
toAddress = destination,
expressProvider = provider,
rateType = rateType,
expressOperationType,
).getOrElse { onExpressError(it); return }
expressOperationType = expressOperationType,
).getOrElse { error -> onExpressError(error); return }
createAndSendCexTransaction(
fromAmount = fromAmount,
@ -141,9 +141,9 @@ internal class SwapTransactionSender @AssistedInject constructor(
destination = swapTransaction.txTo,
userWalletId = userWallet.walletId,
network = fromStatus.currency.network,
).getOrElse {
Timber.e(it, "Failed to create swap CEX tx data")
onSendError(SendTransactionError.UnknownError(Exception(it)))
).getOrElse { error ->
Timber.e(error, "Failed to create swap CEX tx data")
onSendError(SendTransactionError.UnknownError(Exception(error)))
return
}

View file

@ -12,7 +12,7 @@ internal class SendWithSwapConfirmInitialStateTransformer(
return ConfirmUM.Content(
isPrimaryButtonEnabled = false,
isTransactionInProcess = false,
showTapHelp = isShowTapHelp,
isShowTapHelp = isShowTapHelp,
sendingFooter = TextReference.EMPTY,
notifications = persistentListOf(),
tosUM = null,

View file

@ -109,16 +109,16 @@ internal class SendWithSwapConfirmationNotificationsTransformer : Transformer<Se
val expressProvider = (swapAmountUM as? SwapAmountUM.Content)?.selectedQuote?.provider ?: return null
return ConfirmUM.Content.TosUM(
tosLink = expressProvider.termsOfUse?.let {
tosLink = expressProvider.termsOfUse?.let { termsOfUse ->
ConfirmUM.Content.LegalUM(
title = resourceReference(R.string.common_terms_of_use),
link = it,
link = termsOfUse,
)
},
policyLink = expressProvider.privacyPolicy?.let {
policyLink = expressProvider.privacyPolicy?.let { privacyPolicy ->
ConfirmUM.Content.LegalUM(
title = resourceReference(R.string.common_privacy_policy),
link = it,
link = privacyPolicy,
)
},
)

View file

@ -132,8 +132,8 @@ internal class SendWithSwapModel @Inject constructor(
}
override fun resetSendWithSwapNavigation(resetNavigation: Boolean) {
uiState.update {
it.copy(
uiState.update { state ->
state.copy(
destinationUM = DestinationUM.Empty(),
feeSelectorUM = FeeSelectorUM.Loading,
confirmUM = ConfirmUM.Empty,
@ -166,8 +166,8 @@ internal class SendWithSwapModel @Inject constructor(
userWallet = wallet
getPrimaryCurrencyStatusUpdates(params.currency)
},
ifLeft = {
Timber.w(it.toString())
ifLeft = { error ->
Timber.w(error.toString())
swapAlertFactory.getGenericErrorState(
expressError = ExpressError.UnknownError,
onFailedTxEmailClick = {
@ -175,7 +175,7 @@ internal class SendWithSwapModel @Inject constructor(
swapAlertFactory.onFailedTxEmailClick(
userWallet = userWallet,
cryptoCurrency = params.currency,
errorMessage = it.toString(),
errorMessage = error.toString(),
)
}
},

View file

@ -344,7 +344,7 @@ private fun SendWithSwapSuccessContent_Preview() {
txUrl = "https://tangem.com",
provider = ExpressProvider(
providerId = "changelly",
rateTypes = listOf(),
rateTypes = emptyList(),
name = "Changelly",
type = ExpressProviderType.CEX,
imageLarge = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/changelly-1024.png",

View file

@ -36,7 +36,7 @@ internal fun SendWithSwapContent(
stackState: ChildStack<SendWithSwapRoute, ComposableContentComponent>,
onLinkClick: (String) -> Unit,
) {
val navigationUM = navigationUM as? NavigationUM.Content ?: return
val navigationUMContent = navigationUM as? NavigationUM.Content ?: return
Column(
modifier = Modifier
@ -47,9 +47,9 @@ internal fun SendWithSwapContent(
horizontalAlignment = Alignment.CenterHorizontally,
) {
AppBarWithBackButton(
text = navigationUM.title.resolveReference(),
onBackClick = navigationUM.backIconClick,
iconRes = navigationUM.additionalIconRes,
text = navigationUMContent.title.resolveReference(),
onBackClick = navigationUMContent.backIconClick,
iconRes = navigationUMContent.additionalIconRes,
modifier = Modifier.height(TangemTheme.dimens.size56),
)
Children(
@ -93,7 +93,7 @@ internal fun SendWithSwapContent(
)
}
NavigationPrimaryButton(
navigationUM.primaryButton,
navigationUMContent.primaryButton,
modifier = Modifier.padding(
start = 16.dp,
end = 16.dp,