Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-27 18:33:20 +04:00
parent 7bb835032c
commit 68f800a495
47 changed files with 408 additions and 436 deletions

View file

@ -1,7 +1,7 @@
==========================================
Detekt Baseline Updater & Issue Counter
==========================================
Date: 2025-11-26 17:43:14
Date: 2025-11-27 14:13:20
Step 1: Running detekt to check for new issues...
@ -17,13 +17,13 @@ Counting issues in baseline files...
==========================================
Summary:
Total Issues: 1675
Total Issues: 1593
Modules with Issues: 68
Average Issues per Module: 24
Average Issues per Module: 23
Progress:
Fixed: 127 out of 1802 (7%)
Remaining: 1675
Fixed: 209 out of 1802 (11%)
Remaining: 1593
==========================================
All Modules with Issues (sorted by count)
@ -34,7 +34,6 @@ Module Issues
features/wallet/impl 169
features/markets/impl 155
features/onboarding-v2/impl 131
features/onramp/impl 89
features/send-v2/impl 80
features/swap/impl 73
features/hot-wallet/impl 57
@ -74,6 +73,7 @@ features/welcome/impl 8
features/home/impl 8
domain/transaction 8
libs/tangem-sdk-api 7
features/onramp/impl 7
data/txhistory 7
data/tokens 7
data/account 7

View file

@ -2,41 +2,6 @@
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:ConfirmResidencyComponent.kt$ConfirmResidencyComponent.Params$val launchSepa: Boolean</ID>
<ID>BooleanPropertyNaming:DefaultHotCryptoComponent.kt$DefaultHotCryptoComponent$val scrollableContent = when (stack.active.configuration) { OnrampAddTokenRoute.PortfolioSelector -&gt; false OnrampAddTokenRoute.AddToken, OnrampAddTokenRoute.Empty, -&gt; true }</ID>
<ID>BooleanPropertyNaming:OnrampAddTokenComponent.kt$OnrampAddTokenComponent.AddHotCryptoData$val availableMorePortfolio: Boolean</ID>
<ID>BooleanPropertyNaming:OnrampMainComponent.kt$OnrampMainComponent.Params$val launchSepa: Boolean</ID>
<ID>BooleanPropertyNaming:OnrampMainComponentUM.kt$BuyButtonConfig$val enabled: Boolean</ID>
<ID>BooleanPropertyNaming:ProviderListPaymentMethodUM.kt$ProviderListPaymentMethodUM$val enabled: Boolean</ID>
<ID>BooleanPropertyNaming:SelectProviderModel.kt$SelectProviderModel$val allErrorProviders = providers.all { it is OnrampProviderWithQuote.Unavailable.NotSupportedPaymentMethod }</ID>
<ID>MaxChainedCallsOnSameLine:OnrampMainComponentModel.kt$OnrampMainComponentModel$content.amountBlockState.amountFieldModel.fiatAmount.value.isNullOrZero()</ID>
<ID>MaxChainedCallsOnSameLine:OnrampV2MainComponentModel.kt$OnrampV2MainComponentModel$content.amountBlockState.amountFieldModel.fiatAmount.value.isNullOrZero()</ID>
<ID>MultilineLambdaItParameter:AvailableSwapPairsModel.kt$AvailableSwapPairsModel${ handleErrorState( cause = it, networkInfo = params.selectedStatus.value?.toLeastTokenInfo(), currencies = currencies, ) }</ID>
<ID>MultilineLambdaItParameter:AvailableSwapPairsModel.kt$AvailableSwapPairsModel${ handleErrorStateV2( cause = it, networkInfo = params.selectedStatus.value?.toLeastTokenInfo(), accountList = accountList, ) }</ID>
<ID>MultilineLambdaItParameter:AvailableSwapPairsModel.kt$AvailableSwapPairsModel${ it.currency.name.contains(other = query, ignoreCase = true) || it.currency.symbol.contains(other = query, ignoreCase = true) }</ID>
<ID>MultilineLambdaItParameter:AvailableSwapPairsModel.kt$AvailableSwapPairsModel${ it.toMutableMap().apply { put(networkInfo, state) } }</ID>
<ID>MultilineLambdaItParameter:DefaultOnrampComponent.kt$DefaultOnrampComponent${ navigation.push( OnrampChild.RedirectPage( quote = it, cryptoCurrency = params.cryptoCurrency, ), ) }</ID>
<ID>MultilineLambdaItParameter:HotCryptoModel.kt$HotCryptoModel${ Timber.e(it) closeNavigationFlow() }</ID>
<ID>MultilineLambdaItParameter:HotCryptoModel.kt$HotCryptoModel${ bottomSheetNavigation.dismiss() params.onTokenClick(it) }</ID>
<ID>MultilineLambdaItParameter:HotCryptoPortfolioDataLoader.kt$HotCryptoPortfolioDataLoader${ val account: AccountStatus.CryptoPortfolio = when (it) { is AccountStatus.CryptoPortfolio -&gt; it } val addedHotCrypto = mapOfAddedCurrencies[account.account] ?: listOf() HotCryptoPortfolioData.Account( account = account, addedHotCrypto = addedHotCrypto, ) }</ID>
<ID>MultilineLambdaItParameter:OnrampAddToPortfolioModel.kt$OnrampAddToPortfolioModel${ Timber.e("Failed to derive public keys: $it") changeAddButtonProgressStatus(isProgress = false) }</ID>
<ID>MultilineLambdaItParameter:OnrampAddTokenModel.kt$OnrampAddTokenModel${ processError(error = it) uiState.value = um.toggleProgress(false) return@launch }</ID>
<ID>MultilineLambdaItParameter:OnrampMainComponentModel.kt$OnrampMainComponentModel${ (it as? OnrampMainComponentUM.Content)?.copy( errorNotification = null, providerBlockState = OnrampProviderBlockUM.Loading, amountBlockState = it.amountBlockState.copy(secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading), ) ?: it }</ID>
<ID>MultilineLambdaItParameter:OnrampMainComponentModel.kt$OnrampMainComponentModel${ if (it is OnrampMainComponentUM.InitialLoading) { stateFactory.getReadyState(country.defaultCurrency) } else { amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) } }</ID>
<ID>MultilineLambdaItParameter:OnrampMainComponentModel.kt$OnrampMainComponentModel${ it.provider.id == providerState?.providerId &amp;&amp; it.paymentMethod.id == providerState.paymentMethod.id }</ID>
<ID>MultilineLambdaItParameter:OnrampOffersContent.kt${ SpacerH(12.dp) SecondaryButton( modifier = Modifier.fillMaxWidth(), text = it.title.resolveReference(), onClick = it.onClick, ) }</ID>
<ID>MultilineLambdaItParameter:OnrampOffersContent.kt${ Text( modifier = Modifier .background( color = TangemTheme.colors.text.warning.copy(alpha = 0.1f), shape = RoundedCornerShape(4.dp), ) .padding(horizontal = 4.dp), text = it.resolveReference(), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.warning, ) }</ID>
<ID>MultilineLambdaItParameter:OnrampRedirectModel.kt$OnrampRedirectModel${ latestOnrampTransaction = it // Workaround to open Unlimit provider in external browser instead of chrome custom tabs if (params.onrampProviderWithQuote.provider.id.equals(UNLIMIT_PROVIDER_ID, ignoreCase = true)) { urlOpener.openUrlExternalBrowser(it.redirectUrl) } else { urlOpener.openUrl(it.redirectUrl) } }</ID>
<ID>MultilineLambdaItParameter:OnrampTokenItemStateConverterFactory.kt$OnrampTokenItemStateConverterFactory${ TokenItemState.TitleState.Content( text = stringReference(value = it.currency.name), isAvailable = false, ) }</ID>
<ID>MultilineLambdaItParameter:OnrampTokenItemStateConverterFactory.kt$OnrampTokenItemStateConverterFactory${ createSubtitleState( status = it, isAvailable = false, text = unavailableErrorText, ) }</ID>
<ID>MultilineLambdaItParameter:OnrampTokenItemStateConverterFactory.kt$OnrampTokenItemStateConverterFactory${ createSubtitleState( status = it, isAvailable = true, text = stringReference(value = it.currency.symbol), ) }</ID>
<ID>MultilineLambdaItParameter:OnrampTokenItemStateConverterFactory.kt$OnrampTokenItemStateConverterFactory${ createSubtitleState( status = it, text = stringReference(value = it.currency.symbol), isAvailable = false, ) }</ID>
<ID>MultilineLambdaItParameter:OnrampTokenListModel.kt$OnrampTokenListModel${ if (hasRestrictionForSell || isInsufficientBalanceForSell) { mapOf(false to it) } else { it.filterByAvailability() } }</ID>
<ID>MultilineLambdaItParameter:OnrampTokenListModel.kt$OnrampTokenListModel${ it.currency.name.contains(other = query, ignoreCase = true) || it.currency.symbol.contains(other = query, ignoreCase = true) }</ID>
<ID>MultilineLambdaItParameter:OnrampV2MainComponentModel.kt$OnrampV2MainComponentModel${ (it as? OnrampV2MainComponentUM.Content)?.copy( errorNotification = null, offersBlockState = OnrampOffersBlockUM.Loading, amountBlockState = it.amountBlockState.copy(secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty), ) ?: it }</ID>
<ID>MultilineLambdaItParameter:OnrampV2MainComponentModel.kt$OnrampV2MainComponentModel${ when (it) { is OnrampV2MainComponentUM.Content -&gt; { amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) } is OnrampV2MainComponentUM.InitialLoading -&gt; { stateFactory.getReadyState(country.defaultCurrency) } } }</ID>
<ID>MultilineLambdaItParameter:SelectProviderModel.kt$SelectProviderModel${ when (it) { is OnrampProviderWithQuote.Data -&gt; it.toAmount.value // negative difference to sort both when data and unavailable is present is OnrampProviderWithQuote.Unavailable.AmountError -&gt; { when (val error = it.quoteError.error) { is OnrampError.AmountError.TooSmallError -&gt; it.quoteError.fromAmount.value - error.requiredAmount is OnrampError.AmountError.TooBigError -&gt; error.requiredAmount - it.quoteError.fromAmount.value else -&gt; null } } is OnrampProviderWithQuote.Unavailable.NotSupportedPaymentMethod -&gt; null } }</ID>
<ID>NoNameShadowing:OnrampAddTokenUiBuilder.kt$OnrampAddTokenUiBuilder$tokenToAdd</ID>
<ID>NonBooleanPropertyPrefixedWithIs:AvailableSwapPairsModel.kt$AvailableSwapPairsModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase</ID>
<ID>NonBooleanPropertyPrefixedWithIs:HotCryptoModel.kt$HotCryptoModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase</ID>
<ID>NonBooleanPropertyPrefixedWithIs:OnrampAddTokenUiBuilder.kt$OnrampAddTokenUiBuilder$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase</ID>
@ -44,52 +9,5 @@
<ID>NonBooleanPropertyPrefixedWithIs:OnrampOperationModel.kt$OnrampOperationModel$private val isDemoCardUseCase: IsDemoCardUseCase</ID>
<ID>NonBooleanPropertyPrefixedWithIs:OnrampTokenListModel.kt$OnrampTokenListModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase</ID>
<ID>NonBooleanPropertyPrefixedWithIs:SwapSelectTokensModel.kt$SwapSelectTokensModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase</ID>
<ID>NullableBooleanCheck:HotCryptoModel.kt$HotCryptoModel$hotCryptoPortfolioData.wallet.accounts .find { it.account.accountId == accountStatus.accountId } ?.addedHotCrypto ?.none { it.currency.id.rawCurrencyId == hotCrypto.cryptoCurrency.id.rawCurrencyId } ?: false</ID>
<ID>NullableToStringCall:SwapSelectTokensController.kt$SwapSelectTokensController$${transformer::class.simpleName}</ID>
<ID>NullableToStringCall:TokenListUMController.kt$TokenListUMController$${transformer::class.simpleName}</ID>
<ID>PropertyUsedBeforeDeclaration:AllOffersModel.kt$AllOffersModel$state</ID>
<ID>PropertyUsedBeforeDeclaration:CountryListUMController.kt$CountryListUMController$_state</ID>
<ID>PropertyUsedBeforeDeclaration:CurrencyListController.kt$CurrencyListController$_state</ID>
<ID>PropertyUsedBeforeDeclaration:HotCryptoModel.kt$HotCryptoModel$_state</ID>
<ID>PropertyUsedBeforeDeclaration:OnrampAddToPortfolioModel.kt$OnrampAddToPortfolioModel$_state</ID>
<ID>PropertyUsedBeforeDeclaration:OnrampMainComponentModel.kt$OnrampMainComponentModel$_state</ID>
<ID>PropertyUsedBeforeDeclaration:OnrampMainComponentModel.kt$OnrampMainComponentModel$userCountry</ID>
<ID>PropertyUsedBeforeDeclaration:OnrampOperationModel.kt$OnrampOperationModel$_state</ID>
<ID>PropertyUsedBeforeDeclaration:OnrampSelectCountryModel.kt$OnrampSelectCountryModel$controller</ID>
<ID>PropertyUsedBeforeDeclaration:OnrampSelectCurrencyModel.kt$OnrampSelectCurrencyModel$controller</ID>
<ID>PropertyUsedBeforeDeclaration:OnrampSettingsModel.kt$OnrampSettingsModel$_state</ID>
<ID>PropertyUsedBeforeDeclaration:OnrampV2MainComponentModel.kt$OnrampV2MainComponentModel$_state</ID>
<ID>PropertyUsedBeforeDeclaration:SelectProviderModel.kt$SelectProviderModel$_state</ID>
<ID>PropertyUsedBeforeDeclaration:SwapSelectTokensController.kt$SwapSelectTokensController$_state</ID>
<ID>PropertyUsedBeforeDeclaration:SwapSelectTokensModel.kt$SwapSelectTokensModel$_fromCurrencyStatus</ID>
<ID>PropertyUsedBeforeDeclaration:TokenListUMController.kt$TokenListUMController$_state</ID>
<ID>RedundantSuspendModifier:OnrampSelectCountryModel.kt$OnrampSelectCountryModel$suspend</ID>
<ID>ReusedModifierInstance:DefaultOnrampComponent.kt$DefaultOnrampComponent$Content(modifier = modifier)</ID>
<ID>SuspendFunSwallowedCancellation:AvailableSwapPairsModel.kt$AvailableSwapPairsModel$runCatching</ID>
<ID>SuspendFunSwallowedCancellation:OnrampMainComponentModel.kt$OnrampMainComponentModel$runCatching</ID>
<ID>SuspendFunSwallowedCancellation:OnrampSuccessComponentModel.kt$OnrampSuccessComponentModel$runCatching</ID>
<ID>SuspendFunSwallowedCancellation:OnrampV2MainComponentModel.kt$OnrampV2MainComponentModel$runCatching</ID>
<ID>UnnecessaryApply:OnrampFooterContent.kt$apply { AnimatedVisibility( modifier = Modifier .imePadding() .align(Alignment.BottomCenter), visible = state.offersBlockState is OnrampOffersBlockUM.Empty, enter = slideInVertically( initialOffsetY = { it }, animationSpec = tween(durationMillis = 300), ), exit = slideOutVertically( targetOffsetY = { it }, animationSpec = tween(durationMillis = 300), ), label = "Footer block animation", ) { Column( modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { SpacerH(16.dp) OnrampAmountButtons(state = state.onrampAmountButtonUMState) } } }</ID>
<ID>UnnecessaryLet:AllOffersModel.kt$AllOffersModel$let { analyticsEventHandler::send }</ID>
<ID>UnnecessaryLet:HotTokenItemStateConverter.kt$HotTokenItemStateConverter$let { onItemClick -&gt; { onItemClick(it, value) } }</ID>
<ID>UnnecessaryLet:OnrampButtonComponent.kt$let { state.onLinkClick(privacyPolicyLink) }</ID>
<ID>UnnecessaryLet:OnrampButtonComponent.kt$let { state.onLinkClick(termsOfUseLink) }</ID>
<ID>UnnecessaryLet:OnrampTokenListModel.kt$OnrampTokenListModel$let(::resourceReference)</ID>
<ID>UnnecessaryLet:OnrampTokenListUMExt.kt$let(::add)</ID>
<ID>UnnecessaryLet:OnrampV2MainComponentModel.kt$OnrampV2MainComponentModel$let(analyticsEventHandler::send)</ID>
<ID>UnnecessaryLet:SetNothingToFoundStateTransformer.kt$SetNothingToFoundStateTransformer$let(::add)</ID>
<ID>UnnecessaryLet:SetNothingToFoundStateTransformerV2.kt$SetNothingToFoundStateTransformerV2$let(::add)</ID>
<ID>UnsafeCallOnNullableType:DefaultHotCryptoComponent.kt$DefaultHotCryptoComponent$addTokenComponent!!</ID>
<ID>UnsafeCallOnNullableType:DefaultHotCryptoComponent.kt$DefaultHotCryptoComponent$model.portfolioFetcher!!</ID>
<ID>UnsafeCallOnNullableType:DefaultHotCryptoComponent.kt$DefaultHotCryptoComponent$portfolioSelectorComponent!!</ID>
<ID>UseEmptyCounterpart:HotCryptoPortfolioDataLoader.kt$HotCryptoPortfolioDataLoader$listOf()</ID>
<ID>UseEmptyCounterpart:SelectProviderPreviewData.kt$SelectProviderPreviewData$listOf()</ID>
<ID>UseOrEmpty:AvailableSwapPairsModel.kt$AvailableSwapPairsModel$selectedStatus?.currency?.name?.capitalize() ?: ""</ID>
<ID>UseOrEmpty:HotCryptoPortfolioDataLoader.kt$HotCryptoPortfolioDataLoader$getAccountCurrencyStatusUseCase .invokeSync(userWalletId, hotCryptoCurrencies) .getOrNull() ?: emptyMap()</ID>
<ID>UseOrEmpty:HotCryptoPortfolioDataLoader.kt$HotCryptoPortfolioDataLoader$mapOfAddedCurrencies[account.account] ?: listOf()</ID>
<ID>UselessCallOnNotNull:OnrampAddToPortfolioModel.kt$OnrampAddToPortfolioModel$listOfNotNull(params.cryptoCurrency)</ID>
<ID>VarCouldBeVal:AvailableSwapPairsModel.kt$AvailableSwapPairsModel$private var params: AvailableSwapPairsComponent.Params = paramsContainer.require()</ID>
<ID>VarCouldBeVal:OnrampSuccessComponentModel.kt$OnrampSuccessComponentModel$private var expressTxStatusTaskScheduler = SingleTaskScheduler&lt;Unit&gt;()</ID>
<ID>VarCouldBeVal:SwapSelectTokensModel.kt$SwapSelectTokensModel$private var account: Account.CryptoPortfolio? = null</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -9,13 +9,17 @@ import com.tangem.domain.onramp.model.OnrampProviderWithQuote
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.features.onramp.alloffers.AllOffersComponent
import com.tangem.features.onramp.alloffers.entity.AllOffersIntents
import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
import com.tangem.features.onramp.alloffers.entity.AllOffersStateFactory
import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -27,6 +31,9 @@ internal class AllOffersModel @Inject constructor(
paramsContainer: ParamsContainer,
) : Model(), AllOffersIntents {
val state: StateFlow<AllOffersStateUM>
field = MutableStateFlow<AllOffersStateUM>(AllOffersStateUM.Loading)
private var quotesJob: Job? = null
private val stateFactory: AllOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) {
@ -39,9 +46,6 @@ internal class AllOffersModel @Inject constructor(
private val params: AllOffersComponent.Params = paramsContainer.require()
private val _state: MutableStateFlow<AllOffersStateUM> = MutableStateFlow(AllOffersStateUM.Loading)
val state: StateFlow<AllOffersStateUM> = _state.asStateFlow()
init {
subscribeOnAllOffers()
analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened)
@ -53,12 +57,20 @@ internal class AllOffersModel @Inject constructor(
}
override fun onPaymentMethodClicked(paymentMethodId: String) {
val contentState = state.value as? AllOffersStateUM.Content ?: return
val method = contentState.methods.firstOrNull { it.methodConfig.method.id == paymentMethodId } ?: return
analyticsEventHandler.send(
event = OnrampAnalyticsEvent.OnPaymentMethodChosen(paymentMethod = method.methodConfig.method.name),
)
_state.update { contentState.copy(currentMethod = method) }
var method: AllOffersPaymentMethodUM? = null
state.update { contentState ->
if (contentState !is AllOffersStateUM.Content) return
method = contentState.methods.firstOrNull { it.methodConfig.method.id == paymentMethodId } ?: return
contentState.copy(currentMethod = method)
}
if (method != null) {
analyticsEventHandler.send(
event = OnrampAnalyticsEvent.OnPaymentMethodChosen(paymentMethod = method.methodConfig.method.name),
)
}
}
override fun onBuyClick(quote: OnrampProviderWithQuote.Data, onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM) {
@ -69,17 +81,23 @@ internal class AllOffersModel @Inject constructor(
tokenSymbol = params.cryptoCurrency.symbol,
),
)
onrampOfferAdvantagesUM.toAnalyticsEvent(
val event = onrampOfferAdvantagesUM.toAnalyticsEvent(
cryptoCurrencySymbol = params.cryptoCurrency.symbol,
providerName = quote.provider.info.name,
paymentMethodName = quote.paymentMethod.name,
)?.let { analyticsEventHandler::send }
)
if (event != null) {
analyticsEventHandler.send(event)
}
dismiss()
params.openRedirectPage(quote)
}
override fun onBackClicked() {
_state.update { stateFactory.getPaymentsState() }
state.update { stateFactory.getPaymentsState() }
}
override fun onRefresh() {
@ -96,7 +114,7 @@ internal class AllOffersModel @Inject constructor(
maybeOffers.fold(
ifLeft = ::handleOnrampError,
ifRight = { offersGroup ->
_state.update {
state.update {
stateFactory.getLoadedPaymentsState(
methodGroups = offersGroup,
currencyCode = params.amountCurrencyCode,
@ -110,6 +128,6 @@ internal class AllOffersModel @Inject constructor(
private fun handleOnrampError(onrampError: OnrampError) {
Timber.e(onrampError.toString())
_state.update { stateFactory.getOnrampErrorState(onrampError) }
state.update { stateFactory.getOnrampErrorState(onrampError) }
}
}

View file

@ -3,8 +3,8 @@ package com.tangem.features.onramp.confirmresidency
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.onramp.model.OnrampCountry
internal interface ConfirmResidencyComponent : ComposableBottomSheetComponent {
@ -12,7 +12,7 @@ internal interface ConfirmResidencyComponent : ComposableBottomSheetComponent {
val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency,
val country: OnrampCountry,
val launchSepa: Boolean,
val isLaunchSepa: Boolean,
val onDismiss: () -> Unit,
)

View file

@ -57,7 +57,7 @@ internal class ConfirmResidencyModel @Inject constructor(
analyticsEventHandler.send(OnrampAnalyticsEvent.OnResidenceConfirm(country.name))
modelScope.launch {
saveDefaultCountryUseCase.invoke(country)
if (params.launchSepa) {
if (params.isLaunchSepa) {
onrampSaveDefaultCurrencyUseCase.invoke(EUR_CURRENCY)
}

View file

@ -6,7 +6,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -57,7 +60,7 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor(
portfolioSelectorComponentFactory.create(
context = child("portfolioSelectorComponent"),
params = PortfolioSelectorComponent.Params(
portfolioFetcher = model.portfolioFetcher!!,
portfolioFetcher = requireNotNull(model.portfolioFetcher),
controller = model.portfolioSelectorController,
),
)
@ -146,13 +149,13 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor(
end = 16.dp,
bottom = 16.dp,
)
val scrollableContent = when (stack.active.configuration) {
val isScrollableContent = when (stack.active.configuration) {
OnrampAddTokenRoute.PortfolioSelector -> false
OnrampAddTokenRoute.AddToken,
OnrampAddTokenRoute.Empty,
-> true
}
if (scrollableContent) {
if (isScrollableContent) {
Column(
modifier = paddingModifier.verticalScroll(rememberScrollState()),
) {
@ -218,8 +221,8 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor(
}
private fun contentChild(config: OnrampAddTokenRoute): ComposableContentComponent = when (config) {
OnrampAddTokenRoute.AddToken -> addTokenComponent!!
OnrampAddTokenRoute.PortfolioSelector -> portfolioSelectorComponent!!
OnrampAddTokenRoute.AddToken -> requireNotNull(addTokenComponent)
OnrampAddTokenRoute.PortfolioSelector -> requireNotNull(portfolioSelectorComponent)
OnrampAddTokenRoute.Empty -> ComposableContentComponent.EMPTY
}

View file

@ -34,7 +34,7 @@ internal class HotTokenItemStateConverter(
subtitleState = value.quoteStatus.getCryptoPriceState(appCurrency),
fiatAmountState = null,
subtitle2State = null,
onItemClick = onItemClick.let { onItemClick -> { onItemClick(it, value) } },
onItemClick = { onItemClick(it, value) },
onItemLongClick = null,
)
}

View file

@ -83,8 +83,8 @@ internal class HotCryptoModel @Inject constructor(
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
val state: StateFlow<HotCryptoUM> get() = _state
private val _state = MutableStateFlow(value = HotCryptoUM(items = persistentListOf()))
val state: StateFlow<HotCryptoUM>
field = MutableStateFlow(value = HotCryptoUM(items = persistentListOf()))
private val params: HotCryptoComponent.Params = paramsContainer.require()
@ -116,7 +116,7 @@ internal class HotCryptoModel @Inject constructor(
.map(TokensListItemUM::Token)
},
)
.onEach { items -> _state.update { HotCryptoUM(items = it.buildItems(items)) } }
.onEach { items -> state.update { HotCryptoUM(items = it.buildItems(items)) } }
.flowOn(dispatchers.default)
.launchIn(modelScope)
}
@ -131,7 +131,7 @@ internal class HotCryptoModel @Inject constructor(
.map(TokensListItemUM::Token)
}
.onEach { items ->
_state.update { HotCryptoUM(items = it.buildItems(items)) }
state.update { HotCryptoUM(items = it.buildItems(items)) }
}
.launchIn(modelScope)
}
@ -176,7 +176,7 @@ internal class HotCryptoModel @Inject constructor(
cryptoCurrency = currency.cryptoCurrency,
userWallet = userWallet,
account = account,
availableMorePortfolio = false,
isMorePortfolioAvailable = false,
)
hotCryptoToAddDataFlow.emit(tokenToAdd)
bottomSheetNavigationV2.replaceAll(OnrampAddTokenRoute.AddToken)
@ -192,7 +192,7 @@ internal class HotCryptoModel @Inject constructor(
cryptoCurrency = currency.cryptoCurrency,
userWallet = userWallet,
account = selectedAccount,
availableMorePortfolio = true,
isMorePortfolioAvailable = true,
)
}
.shareIn(this, started = SharingStarted.Eagerly)
@ -218,8 +218,8 @@ internal class HotCryptoModel @Inject constructor(
closeNavigationFlow()
channel.close()
}
.catch {
Timber.e(it)
.catch { throwable ->
Timber.e(throwable)
closeNavigationFlow()
}
.launchIn(modelScope)
@ -232,9 +232,9 @@ internal class HotCryptoModel @Inject constructor(
userWalletId = params.userWalletId,
cryptoCurrencyId = id,
)
.onRight {
.onRight { status ->
bottomSheetNavigation.dismiss()
params.onTokenClick(it)
params.onTokenClick(status)
}
.onLeft { Timber.d("Unable to get CryptoCurrencyStatus[$id]: $it") }
}
@ -246,8 +246,8 @@ internal class HotCryptoModel @Inject constructor(
val isNotAddedHotCrypto = hotCryptoPortfolioData.wallet.accounts
.find { it.account.accountId == accountStatus.accountId }
?.addedHotCrypto
?.none { it.currency.id.rawCurrencyId == hotCrypto.cryptoCurrency.id.rawCurrencyId }
?: false
?.none { it.currency.id.rawCurrencyId == hotCrypto.cryptoCurrency.id.rawCurrencyId } == true
return@isEnabled isNotAddedHotCrypto
}
}

View file

@ -41,12 +41,12 @@ internal class HotCryptoPortfolioDataLoader @Inject constructor(
val mapOfAddedCurrencies: AccountCryptoCurrencyStatuses = getAccountCurrencyStatusUseCase
.invokeSync(userWalletId, hotCryptoCurrencies)
.getOrNull()
?: emptyMap()
val accountsWithHotCrypto = walletAccounts.accountStatuses.map {
val account: AccountStatus.CryptoPortfolio = when (it) {
is AccountStatus.CryptoPortfolio -> it
.orEmpty()
val accountsWithHotCrypto = walletAccounts.accountStatuses.map { accountStatus ->
val account: AccountStatus.CryptoPortfolio = when (accountStatus) {
is AccountStatus.CryptoPortfolio -> accountStatus
}
val addedHotCrypto = mapOfAddedCurrencies[account.account] ?: listOf()
val addedHotCrypto = mapOfAddedCurrencies[account.account].orEmpty()
HotCryptoPortfolioData.Account(
account = account,
addedHotCrypto = addedHotCrypto,

View file

@ -44,7 +44,7 @@ internal class OnrampAddTokenComponent @AssistedInject constructor(
val cryptoCurrency: CryptoCurrency,
val userWallet: UserWallet,
val account: AccountStatus,
val availableMorePortfolio: Boolean,
val isMorePortfolioAvailable: Boolean,
)
interface Callbacks {

View file

@ -56,7 +56,7 @@ internal class OnrampAddTokenUiBuilder @Inject constructor(
icon = accountIcon,
name = portfolioName,
isAccountMode = isAccountMode,
isMultiChoice = tokenToAdd.availableMorePortfolio,
isMultiChoice = tokenToAdd.isMorePortfolioAvailable,
onClick = { params.callbacks.onChangePortfolioClick() },
)
}
@ -76,7 +76,7 @@ internal class OnrampAddTokenUiBuilder @Inject constructor(
val networkUM = createNetwork(tokenToAdd)
val portfolioUM = createPortfolio(tokenToAdd)
val currency = tokenToAdd.cryptoCurrency
val tokenToAdd = TokenItemState.Content(
val tokenItemState = TokenItemState.Content(
id = currency.id.value,
iconState = CryptoCurrencyToIconStateConverter().convert(currency),
titleState = TokenItemState.TitleState.Content(stringReference(currency.name)),
@ -87,7 +87,7 @@ internal class OnrampAddTokenUiBuilder @Inject constructor(
onItemLongClick = null,
)
return AddTokenUM(
tokenToAdd = tokenToAdd,
tokenToAdd = tokenItemState,
network = networkUM,
portfolio = portfolioUM,
button = button,

View file

@ -40,8 +40,8 @@ internal class OnrampAddToPortfolioModel @Inject constructor(
private val params: OnrampAddToPortfolioComponent.Params = paramsContainer.require()
val state: StateFlow<OnrampAddToPortfolioUM> get() = _state
private val _state = MutableStateFlow(value = getInitialState())
val state: StateFlow<OnrampAddToPortfolioUM>
field = MutableStateFlow(value = getInitialState())
private fun getInitialState(): OnrampAddToPortfolioUM {
return OnrampAddToPortfolioUM(
@ -72,8 +72,11 @@ internal class OnrampAddToPortfolioModel @Inject constructor(
private fun onAddClick() {
modelScope.launch {
changeAddButtonProgressStatus(isProgress = true)
derivePublicKeysUseCase(params.userWalletId, listOfNotNull(params.cryptoCurrency)).getOrElse {
Timber.e("Failed to derive public keys: $it")
derivePublicKeysUseCase(
userWalletId = params.userWalletId,
currencies = listOf(params.cryptoCurrency),
).getOrElse { throwable ->
Timber.e("Failed to derive public keys: $throwable")
changeAddButtonProgressStatus(isProgress = false)
}
@ -88,8 +91,8 @@ internal class OnrampAddToPortfolioModel @Inject constructor(
}
private fun changeAddButtonProgressStatus(isProgress: Boolean) {
_state.update {
it.copy(addButtonUM = it.addButtonUM.copy(isProgress = isProgress))
state.update { prevState ->
prevState.copy(addButtonUM = prevState.addButtonUM.copy(isProgress = isProgress))
}
}
}

View file

@ -69,8 +69,8 @@ internal class OnrampAddTokenModel @Inject constructor(
val cryptoCurrency = tokenToAdd.cryptoCurrency
val accountId = tokenToAdd.account.accountId
manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency)
.onLeft {
processError(error = it)
.onLeft { throwable ->
processError(error = throwable)
uiState.value = um.toggleProgress(false)
return@launch
}

View file

@ -57,7 +57,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor(
userWalletId = params.userWalletId,
cryptoCurrency = params.cryptoCurrency,
country = config.country,
launchSepa = params.launchSepa,
isLaunchSepa = params.isLaunchSepa,
onDismiss = {
model.bottomSheetNavigation.dismiss()
model.handleOnrampAvailable()

View file

@ -3,9 +3,9 @@ package com.tangem.features.onramp.main
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.models.wallet.UserWalletId
internal interface OnrampMainComponent : ComposableContentComponent {
@ -15,7 +15,7 @@ internal interface OnrampMainComponent : ComposableContentComponent {
val source: OnrampSource,
val openSettings: () -> Unit,
val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit,
val launchSepa: Boolean,
val isLaunchSepa: Boolean,
)
interface Factory : ComponentFactory<Params, OnrampMainComponent>

View file

@ -38,7 +38,7 @@ internal sealed interface OnrampMainComponentUM {
override val buyButtonConfig: BuyButtonConfig = BuyButtonConfig(
text = resourceReference(R.string.common_buy),
onClick = {},
enabled = false,
isEnabled = false,
)
}
@ -54,5 +54,5 @@ internal sealed interface OnrampMainComponentUM {
internal data class BuyButtonConfig(
val text: TextReference,
val onClick: () -> Unit,
val enabled: Boolean,
val isEnabled: Boolean,
)

View file

@ -72,7 +72,7 @@ internal class OnrampStateFactory(
val contentState = state as? OnrampMainComponentUM.Content ?: return state
return contentState.copy(
buyButtonConfig = contentState.buyButtonConfig.copy(enabled = false),
buyButtonConfig = contentState.buyButtonConfig.copy(isEnabled = false),
amountBlockState = contentState.amountBlockState.copy(
amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true),
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Error(
@ -92,7 +92,7 @@ internal class OnrampStateFactory(
return when (state) {
is OnrampMainComponentUM.Content -> state.copy(
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
buyButtonConfig = state.buyButtonConfig.copy(enabled = false),
buyButtonConfig = state.buyButtonConfig.copy(isEnabled = false),
amountBlockState = state.amountBlockState.copy(
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY),
),

View file

@ -66,7 +66,7 @@ internal class OnrampAmountFieldChangeConverter(
amountFieldModel = amountFieldModel,
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY),
),
buyButtonConfig = buyButtonConfig.copy(enabled = false),
buyButtonConfig = buyButtonConfig.copy(isEnabled = false),
providerBlockState = OnrampProviderBlockUM.Empty,
)
}

View file

@ -69,7 +69,7 @@ internal class OnrampAmountStateFactory(
return currentState.copy(
amountBlockState = amountState.copy(secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading),
providerBlockState = OnrampProviderBlockUM.Loading,
buyButtonConfig = currentState.buyButtonConfig.copy(enabled = false),
buyButtonConfig = currentState.buyButtonConfig.copy(isEnabled = false),
errorNotification = null,
)
}
@ -87,7 +87,7 @@ internal class OnrampAmountStateFactory(
secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState) ?: amountState.secondaryFieldModel,
),
buyButtonConfig = currentState.buyButtonConfig.copy(
enabled = quote is OnrampQuote.Data,
isEnabled = quote is OnrampQuote.Data,
onClick = {
if (quote is OnrampQuote.Data) {
onrampIntents.onBuyClick(
@ -166,7 +166,7 @@ internal class OnrampAmountStateFactory(
onLinkClick = onrampIntents::onLinkClick,
),
buyButtonConfig = currentState.buyButtonConfig.copy(
enabled = providerResult is SelectProviderResult.ProviderWithQuote,
isEnabled = providerResult is SelectProviderResult.ProviderWithQuote,
onClick = {
if (providerResult is SelectProviderResult.ProviderWithQuote) {
onrampIntents.onBuyClick(

View file

@ -14,17 +14,17 @@ import com.tangem.core.ui.components.fields.InputManager
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.*
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
import com.tangem.domain.onramp.model.OnrampAvailability
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
import com.tangem.domain.onramp.model.OnrampQuote
import com.tangem.domain.onramp.model.error.OnrampError
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.models.wallet.UserWallet
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.onramp.main.OnrampMainComponent
import com.tangem.features.onramp.main.entity.*
@ -38,6 +38,7 @@ import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.PeriodicTask
import com.tangem.utils.coroutines.SingleTaskScheduler
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -69,35 +70,40 @@ internal class OnrampMainComponentModel @Inject constructor(
private val params: OnrampMainComponent.Params = paramsContainer.require()
private var shouldForceChooseSepa = params.launchSepa
private var shouldForceChooseSepa = params.isLaunchSepa
private var currencyToRestore: OnrampCurrency? = null
val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
val bottomSheetNavigation: SlotNavigation<OnrampMainBottomSheetConfig> = SlotNavigation()
private val lastUpdateState = mutableStateOf<OnrampLastUpdate?>(null)
private var userCountry: UserCountry? = null
@Suppress("PropertyUsedBeforeDeclaration")
private val stateFactory = OnrampStateFactory(
currentStateProvider = Provider { _state.value },
currentStateProvider = Provider { state.value },
cryptoCurrency = params.cryptoCurrency,
onrampIntents = this,
)
val state: StateFlow<OnrampMainComponentUM>
field = MutableStateFlow<OnrampMainComponentUM>(
value = stateFactory.getInitialState(
currency = params.cryptoCurrency.name,
onClose = ::onCloseClick,
),
)
private val amountStateFactory = OnrampAmountStateFactory(
currentStateProvider = Provider { _state.value },
currentStateProvider = Provider { state.value },
analyticsEventHandler = analyticsEventHandler,
onrampIntents = this,
cryptoCurrency = params.cryptoCurrency,
needApplyFCARestrictions = Provider { userCountry.needApplyFCARestrictions() },
)
private val _state: MutableStateFlow<OnrampMainComponentUM> = MutableStateFlow(
value = stateFactory.getInitialState(
currency = params.cryptoCurrency.name,
onClose = ::onCloseClick,
),
)
private val quotesTaskScheduler = SingleTaskScheduler<Unit>()
val state: StateFlow<OnrampMainComponentUM> get() = _state.asStateFlow()
val bottomSheetNavigation: SlotNavigation<OnrampMainBottomSheetConfig> = SlotNavigation()
private val lastUpdateState = mutableStateOf<OnrampLastUpdate?>(null)
private var userCountry: UserCountry? = null
private val quotesTaskScheduler = SingleTaskScheduler<Unit>()
init {
userCountry = getUserCountryUseCase.invokeSync().getOrNull()
@ -106,7 +112,7 @@ internal class OnrampMainComponentModel @Inject constructor(
modelScope.launch {
clearOnrampCacheUseCase()
if (params.launchSepa) {
if (params.isLaunchSepa) {
currencyToRestore = onrampGetDefaultCurrencyUseCase.invoke().getOrNull()
onrampSaveDefaultCurrencyUseCase.invoke(EUR_CURRENCY)
}
@ -132,7 +138,7 @@ internal class OnrampMainComponentModel @Inject constructor(
}
fun onProviderSelected(result: SelectProviderResult, isBestRate: Boolean) {
_state.update { amountStateFactory.getAmountSecondaryUpdatedState(result, isBestRate) }
state.update { amountStateFactory.getAmountSecondaryUpdatedState(result, isBestRate) }
if (result.paymentMethod.id != SEPA_METHOD_ID) {
shouldForceChooseSepa = false
@ -164,9 +170,9 @@ internal class OnrampMainComponentModel @Inject constructor(
ifRight = { country ->
if (country == null) return@onEach
val wasInitialLoading = _state.value is OnrampMainComponentUM.InitialLoading
_state.update {
if (it is OnrampMainComponentUM.InitialLoading) {
val wasInitialLoading = state.value is OnrampMainComponentUM.InitialLoading
state.update { prevState ->
if (prevState is OnrampMainComponentUM.InitialLoading) {
stateFactory.getReadyState(country.defaultCurrency)
} else {
amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency)
@ -175,7 +181,7 @@ internal class OnrampMainComponentModel @Inject constructor(
updatePairsAndQuotes()
if (wasInitialLoading && params.launchSepa) {
if (wasInitialLoading && params.isLaunchSepa) {
onAmountValueChanged(value = PREDEFINED_SEPA_AMOUNT, isValuePasted = true)
}
},
@ -188,20 +194,25 @@ internal class OnrampMainComponentModel @Inject constructor(
amountInputManager.query
.filter(String::isNotEmpty)
.collectLatest { _ ->
_state.update { amountStateFactory.getAmountSecondaryLoadingState() }
state.update { amountStateFactory.getAmountSecondaryLoadingState() }
startLoadingQuotes()
}
}
private suspend fun updatePairsAndQuotes() {
val state = state.value as? OnrampMainComponentUM.Content
state.update { prevState ->
val contentState = state.value as? OnrampMainComponentUM.Content ?: return@update prevState
if (!state?.amountBlockState?.amountFieldModel?.fiatValue.isNullOrEmpty()) {
_state.update { amountStateFactory.getAmountSecondaryLoadingState() }
if (contentState.amountBlockState.amountFieldModel.fiatValue.isNotEmpty()) {
amountStateFactory.getAmountSecondaryLoadingState()
} else {
prevState
}
}
fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold(
ifLeft = ::handleOnrampError,
ifRight = { _state.update { amountStateFactory.getAmountSecondaryResetState() } },
ifRight = { state.update { amountStateFactory.getAmountSecondaryResetState() } },
)
startLoadingQuotes()
}
@ -209,7 +220,7 @@ internal class OnrampMainComponentModel @Inject constructor(
private fun handleOnrampError(onrampError: OnrampError) {
Timber.e(onrampError.toString())
sendOnrampErrorAnalytic(onrampError)
_state.update { stateFactory.getOnrampErrorState(onrampError) }
state.update { stateFactory.getOnrampErrorState(onrampError) }
}
private fun startLoadingQuotes() {
@ -221,12 +232,15 @@ internal class OnrampMainComponentModel @Inject constructor(
return PeriodicTask(
delay = UPDATE_DELAY,
task = {
runCatching {
val content = state.value as? OnrampMainComponentUM.Content ?: return@runCatching
if (content.amountBlockState.amountFieldModel.fiatAmount.value.isNullOrZero()) return@runCatching
runSuspendCatching {
val content = state.value as? OnrampMainComponentUM.Content ?: return@runSuspendCatching
val amountBlockState = content.amountBlockState
if (amountBlockState.amountFieldModel.fiatAmount.value.isNullOrZero()) {
return@runSuspendCatching
}
fetchQuotesUseCase.invoke(
userWallet = userWallet,
amount = content.amountBlockState.amountFieldModel.fiatAmount,
amount = amountBlockState.amountFieldModel.fiatAmount,
cryptoCurrency = params.cryptoCurrency,
).onLeft(::handleOnrampError)
}
@ -249,7 +263,7 @@ internal class OnrampMainComponentModel @Inject constructor(
}
override fun onAmountValueChanged(value: String, isValuePasted: Boolean) {
_state.update { amountStateFactory.getOnAmountValueChange(value, isValuePasted) }
state.update { amountStateFactory.getOnAmountValueChange(value, isValuePasted) }
modelScope.launch { amountInputManager.update(value) }
}
@ -290,7 +304,7 @@ internal class OnrampMainComponentModel @Inject constructor(
}
override fun onRefresh() {
_state.update {
state.update {
stateFactory.getInitialState(
currency = params.cryptoCurrency.name,
onClose = router::pop,
@ -310,7 +324,7 @@ internal class OnrampMainComponentModel @Inject constructor(
quotesTaskScheduler.cancelTask()
modelScope.launch {
if (params.launchSepa) {
if (params.isLaunchSepa) {
currencyToRestore?.let { onrampSaveDefaultCurrencyUseCase.invoke(it) }
}
}
@ -329,11 +343,11 @@ internal class OnrampMainComponentModel @Inject constructor(
val quote = selectOrUpdateQuote(quotes)
if (quote == null) {
_state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
lastUpdateState.value = null
return
}
_state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote = quote) }
state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote = quote) }
}
/**
@ -346,7 +360,7 @@ internal class OnrampMainComponentModel @Inject constructor(
private fun selectOrUpdateQuote(quotes: List<OnrampQuote>): OnrampQuote? {
val quoteToCheck = quotes.firstOrNull { it !is OnrampQuote.Error }
val bestSepaQuote = if (params.launchSepa && shouldForceChooseSepa) {
val bestSepaQuote = if (params.isLaunchSepa && shouldForceChooseSepa) {
quotes.filterIsInstance<OnrampQuote.Data>()
.filter { it.paymentMethod.id == SEPA_METHOD_ID }
.maxByOrNull { it.toAmount.value }
@ -362,9 +376,9 @@ internal class OnrampMainComponentModel @Inject constructor(
val providerState = state?.providerBlockState as? OnrampProviderBlockUM.Content
// Get current selected quote to update
val lastSelectedQuote = quotes.firstOrNull {
it.provider.id == providerState?.providerId &&
it.paymentMethod.id == providerState.paymentMethod.id
val lastSelectedQuote = quotes.firstOrNull { quote ->
quote.provider.id == providerState?.providerId &&
quote.paymentMethod.id == providerState.paymentMethod.id
}
// Check if selected updated quote is not error
@ -392,18 +406,20 @@ internal class OnrampMainComponentModel @Inject constructor(
shouldForceChooseSepa = false
}
_state.update {
state.update {
amountStateFactory.getUpdatedProviderState(selectedQuote = quote, quotes = quotes)
}
}
private fun onRetryQuotes() {
_state.update {
(it as? OnrampMainComponentUM.Content)?.copy(
state.update { prevState ->
(prevState as? OnrampMainComponentUM.Content)?.copy(
errorNotification = null,
providerBlockState = OnrampProviderBlockUM.Loading,
amountBlockState = it.amountBlockState.copy(secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading),
) ?: it
amountBlockState = prevState.amountBlockState.copy(
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading,
),
) ?: prevState
}
startLoadingQuotes()
}

View file

@ -37,7 +37,7 @@ internal fun OnrampButtonComponent(state: OnrampMainComponentUM) {
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(id = R.string.common_buy),
onClick = state.buyButtonConfig.onClick,
enabled = state.buyButtonConfig.enabled,
enabled = state.buyButtonConfig.isEnabled,
)
}
}
@ -78,19 +78,23 @@ private fun OnrampTosText(provider: OnrampProviderBlockUM.Content?) {
textAlign = TextAlign.Center,
),
onClick = { offset ->
clickableAnnotation.getStringAnnotations(
val tosAnnotations = clickableAnnotation.getStringAnnotations(
tag = TERMS_OF_USE_KEY,
start = offset,
end = offset,
).firstOrNull()?.let {
)
if (tosAnnotations.any()) {
state.onLinkClick(termsOfUseLink)
}
clickableAnnotation.getStringAnnotations(
val privacyPolicyAnnotations = clickableAnnotation.getStringAnnotations(
tag = PRIVACY_POLICY_KEY,
start = offset,
end = offset,
).firstOrNull()?.let {
)
if (privacyPolicyAnnotations.any()) {
state.onLinkClick(privacyPolicyLink)
}
},

View file

@ -63,7 +63,7 @@ internal class DefaultOnrampV2MainComponent @AssistedInject constructor(
userWalletId = params.userWalletId,
cryptoCurrency = params.cryptoCurrency,
country = config.country,
launchSepa = false,
isLaunchSepa = false,
onDismiss = {
model.bottomSheetNavigation.dismiss()
model.handleOnrampAvailable()

View file

@ -25,6 +25,7 @@ import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.PeriodicTask
import com.tangem.utils.coroutines.SingleTaskScheduler
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -54,41 +55,43 @@ internal class OnrampV2MainComponentModel @Inject constructor(
OnrampAmountButtonUMStateFactory()
}
private val onrampOffersStateFactory: OnrampOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) {
OnrampOffersStateFactory(
currentStateProvider = Provider { _state.value },
onrampIntents = this,
)
}
@Suppress("PropertyUsedBeforeDeclaration")
private val stateFactory: OnrampV2StateFactory by lazy(LazyThreadSafetyMode.NONE) {
OnrampV2StateFactory(
currentStateProvider = Provider { _state.value },
currentStateProvider = Provider { state.value },
cryptoCurrency = params.cryptoCurrency,
onrampIntents = this,
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
)
}
val state: StateFlow<OnrampV2MainComponentUM>
field = MutableStateFlow<OnrampV2MainComponentUM>(
value = stateFactory.getInitialState(
currency = params.cryptoCurrency.name,
onClose = ::onCloseClick,
openSettings = ::openSettings,
),
)
private val amountStateFactory: OnrampV2AmountStateFactory by lazy(LazyThreadSafetyMode.NONE) {
OnrampV2AmountStateFactory(
currentStateProvider = Provider { _state.value },
currentStateProvider = Provider { state.value },
analyticsEventHandler = analyticsEventHandler,
onrampIntents = this,
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
)
}
private val _state: MutableStateFlow<OnrampV2MainComponentUM> = MutableStateFlow(
value = stateFactory.getInitialState(
currency = params.cryptoCurrency.name,
onClose = ::onCloseClick,
openSettings = ::openSettings,
),
)
private val onrampOffersStateFactory: OnrampOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) {
OnrampOffersStateFactory(
currentStateProvider = Provider { state.value },
onrampIntents = this,
)
}
private val quotesTaskScheduler = SingleTaskScheduler<Unit>()
val state: StateFlow<OnrampV2MainComponentUM> get() = _state.asStateFlow()
val bottomSheetNavigation: SlotNavigation<OnrampV2MainBottomSheetConfig> = SlotNavigation()
val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
@ -112,7 +115,7 @@ internal class OnrampV2MainComponentModel @Inject constructor(
}
override fun onAmountValueChanged(value: String) {
_state.update { amountStateFactory.getOnAmountValueChange(value) }
state.update { amountStateFactory.getOnAmountValueChange(value) }
modelScope.launch { amountInputManager.update(value) }
}
@ -153,7 +156,7 @@ internal class OnrampV2MainComponentModel @Inject constructor(
}
override fun onRefresh() {
_state.update {
state.update {
stateFactory.getInitialState(
currency = params.cryptoCurrency.name,
onClose = router::pop,
@ -185,12 +188,16 @@ internal class OnrampV2MainComponentModel @Inject constructor(
return PeriodicTask(
delay = UPDATE_DELAY,
task = {
runCatching {
val content = state.value as? OnrampV2MainComponentUM.Content ?: return@runCatching
if (content.amountBlockState.amountFieldModel.fiatAmount.value.isNullOrZero()) return@runCatching
runSuspendCatching {
val amountBlockState = (state.value as? OnrampV2MainComponentUM.Content)?.amountBlockState
?: return@runSuspendCatching
val fiatAmount = amountBlockState.amountFieldModel.fiatAmount
if (fiatAmount.value.isNullOrZero()) return@runSuspendCatching
fetchQuotesUseCase.invoke(
userWallet = userWallet,
amount = content.amountBlockState.amountFieldModel.fiatAmount,
amount = amountBlockState.amountFieldModel.fiatAmount,
cryptoCurrency = params.cryptoCurrency,
).onLeft(::handleOnrampError)
}
@ -230,7 +237,7 @@ internal class OnrampV2MainComponentModel @Inject constructor(
ifLeft = ::handleOnrampError,
ifRight = { offers ->
if (offers.isNotEmpty()) {
_state.update { onrampOffersStateFactory.getOffersState(offers) }
state.update { onrampOffersStateFactory.getOffersState(offers) }
}
},
)
@ -252,8 +259,8 @@ internal class OnrampV2MainComponentModel @Inject constructor(
ifLeft = ::handleOnrampError,
ifRight = { country ->
if (country == null) return@onEach
_state.update {
when (it) {
state.update { prevState ->
when (prevState) {
is OnrampV2MainComponentUM.Content -> {
amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency)
}
@ -285,27 +292,29 @@ internal class OnrampV2MainComponentModel @Inject constructor(
sendOnrampQuotesErrorAnalytic(quotes)
when {
quotes.isEmpty() -> {
_state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
}
quotes.all { it is OnrampQuote.AmountError } -> {
_state.update { amountStateFactory.getSecondaryFieldAmountErrorState(quotes) }
state.update { amountStateFactory.getSecondaryFieldAmountErrorState(quotes) }
}
quotes.none { it is OnrampQuote.Data } -> {
_state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
}
else -> {
_state.update { amountStateFactory.getAmountSecondaryFieldResetState() }
state.update { amountStateFactory.getAmountSecondaryFieldResetState() }
}
}
}
private fun onRetryQuotes() {
_state.update {
(it as? OnrampV2MainComponentUM.Content)?.copy(
state.update { prevState ->
(prevState as? OnrampV2MainComponentUM.Content)?.copy(
errorNotification = null,
offersBlockState = OnrampOffersBlockUM.Loading,
amountBlockState = it.amountBlockState.copy(secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty),
) ?: it
amountBlockState = prevState.amountBlockState.copy(
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty,
),
) ?: prevState
}
startLoadingQuotes()
}
@ -314,7 +323,7 @@ internal class OnrampV2MainComponentModel @Inject constructor(
fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold(
ifLeft = ::handleOnrampError,
ifRight = {
_state.update {
state.update {
amountStateFactory.getAmountSecondaryFieldResetState()
}
startLoadingQuotes()
@ -324,7 +333,7 @@ internal class OnrampV2MainComponentModel @Inject constructor(
private fun handleOnrampError(onrampError: OnrampError) {
Timber.e(onrampError.toString())
_state.update { stateFactory.getOnrampErrorState(onrampError) }
state.update { stateFactory.getOnrampErrorState(onrampError) }
}
private fun sendOnrampQuotesErrorAnalytic(quotes: List<OnrampQuote>) {
@ -361,22 +370,26 @@ internal class OnrampV2MainComponentModel @Inject constructor(
onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM,
categoryUM: OnrampOfferCategoryUM,
) {
when (categoryUM) {
val event = when (categoryUM) {
OnrampOfferCategoryUM.RecentlyUsed -> {
OnrampAnalyticsEvent.RecentlyBuyClicked(
tokenSymbol = params.cryptoCurrency.symbol,
providerName = quote.provider.info.name,
paymentMethod = quote.paymentMethod.name,
).let(analyticsEventHandler::send)
)
}
OnrampOfferCategoryUM.Recommended -> {
onrampOfferAdvantagesUM.toAnalyticsEvent(
cryptoCurrencySymbol = params.cryptoCurrency.symbol,
providerName = quote.provider.info.name,
paymentMethodName = quote.paymentMethod.name,
)?.let(analyticsEventHandler::send)
)
}
}
if (event != null) {
analyticsEventHandler.send(event)
}
}
private companion object {

View file

@ -25,34 +25,28 @@ import com.tangem.features.onramp.mainv2.entity.OnrampV2AmountButtonUMState
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM
@Composable
internal fun OnrampFooterContent(
state: OnrampV2MainComponentUM.Content,
boxScope: BoxScope,
modifier: Modifier = Modifier,
) {
boxScope.apply {
AnimatedVisibility(
modifier = Modifier
.imePadding()
.align(Alignment.BottomCenter),
visible = state.offersBlockState is OnrampOffersBlockUM.Empty,
enter = slideInVertically(
initialOffsetY = { it },
animationSpec = tween(durationMillis = 300),
),
exit = slideOutVertically(
targetOffsetY = { it },
animationSpec = tween(durationMillis = 300),
),
label = "Footer block animation",
internal fun BoxScope.OnrampFooterContent(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) {
AnimatedVisibility(
modifier = modifier
.imePadding()
.align(Alignment.BottomCenter),
visible = state.offersBlockState is OnrampOffersBlockUM.Empty,
enter = slideInVertically(
initialOffsetY = { it },
animationSpec = tween(durationMillis = 300),
),
exit = slideOutVertically(
targetOffsetY = { it },
animationSpec = tween(durationMillis = 300),
),
label = "Footer block animation",
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SpacerH(16.dp)
OnrampAmountButtons(state = state.onrampAmountButtonUMState)
}
SpacerH(16.dp)
OnrampAmountButtons(state = state.onrampAmountButtonUMState)
}
}
}

View file

@ -60,10 +60,7 @@ internal fun OnrampNewMainComponentContent(state: OnrampV2MainComponentUM, modif
}
if (state is OnrampV2MainComponentUM.Content) {
OnrampFooterContent(
state = state,
boxScope = this,
)
OnrampFooterContent(state = state)
}
}
}

View file

@ -79,12 +79,12 @@ internal fun OnrampOffersContent(state: OnrampOffersBlockUM) {
}
}
state.onrampAllOffersButtonConfig?.let {
if (state.onrampAllOffersButtonConfig != null) {
SpacerH(12.dp)
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = it.title.resolveReference(),
onClick = it.onClick,
text = state.onrampAllOffersButtonConfig.title.resolveReference(),
onClick = state.onrampAllOffersButtonConfig.onClick,
)
}
}
@ -212,7 +212,8 @@ private fun RateBlock(rate: String, diff: TextReference?, isOfferUnavailable: Bo
color = if (isOfferUnavailable) TangemTheme.colors.text.tertiary else TangemTheme.colors.text.primary1,
modifier = Modifier.testTag(OnrampOffersBlockTestTags.OFFER_TOKEN_AMOUNT),
)
diff?.let {
if (diff != null) {
Text(
modifier = Modifier
.background(
@ -220,7 +221,7 @@ private fun RateBlock(rate: String, diff: TextReference?, isOfferUnavailable: Bo
shape = RoundedCornerShape(4.dp),
)
.padding(horizontal = 4.dp),
text = it.resolveReference(),
text = diff.resolveReference(),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.warning,
)

View file

@ -4,6 +4,6 @@ internal data class ProviderListPaymentMethodUM(
val id: String,
val name: String,
val imageUrl: String,
val enabled: Boolean,
val isEnabled: Boolean,
val onClick: () -> Unit,
)

View file

@ -36,7 +36,6 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
@ -55,10 +54,11 @@ internal class SelectProviderModel @Inject constructor(
paramsContainer: ParamsContainer,
) : Model() {
val state: StateFlow<SelectPaymentAndProviderUM> get() = _state.asStateFlow()
val state: StateFlow<SelectPaymentAndProviderUM>
field = MutableStateFlow(getInitialState())
val bottomSheetNavigation: SlotNavigation<ProviderListBottomSheetConfig> = SlotNavigation()
private val params: SelectProviderComponent.Params = paramsContainer.require()
private val _state = MutableStateFlow(getInitialState())
private var userCountry: UserCountry? = null
@ -84,18 +84,18 @@ internal class SelectProviderModel @Inject constructor(
val filteredEmptyMethods = methods.mapNotNull { method ->
val providers = getOnrampProviderWithQuoteUseCase(method).getOrNull()
if (!providers.isNullOrEmpty()) {
val allErrorProviders = providers.all {
val isAllErrorProviders = providers.all {
it is OnrampProviderWithQuote.Unavailable.NotSupportedPaymentMethod
}
PaymentProviderUM(
paymentMethod = method,
providers = providers.toProvidersListItems(),
).takeIf { !allErrorProviders }
).takeIf { !isAllErrorProviders }
} else {
null
}
}
_state.value = state.value.copy(
state.value = state.value.copy(
paymentMethods = filteredEmptyMethods.toPersistentList(),
isPaymentMethodClickEnabled = filteredEmptyMethods.isNotEmpty(),
)
@ -106,7 +106,7 @@ internal class SelectProviderModel @Inject constructor(
modelScope.launch {
getOnrampProviderWithQuoteUseCase.invoke(paymentMethod)
.onRight { quotes ->
_state.update { state ->
state.update { state ->
state.copy(
selectedPaymentMethod = state.selectedPaymentMethod.copy(
providers = quotes.toProvidersListItems(),
@ -163,7 +163,7 @@ internal class SelectProviderModel @Inject constructor(
val firstProvider = methodContainer.providers.firstOrNull()
analyticsEventHandler.send(OnrampAnalyticsEvent.OnPaymentMethodChosen(paymentMethod = paymentMethod.name))
modelScope.launch {
_state.update { state ->
state.update { state ->
state.copy(
selectedPaymentMethod = state.selectedPaymentMethod.copy(
paymentMethod = paymentMethod,
@ -279,7 +279,7 @@ internal class SelectProviderModel @Inject constructor(
tokenSymbol = params.cryptoCurrency.symbol,
),
)
_state.update {
state.update {
it.copy(selectedProviderId = result.provider.id)
}
params.onProviderClick(result, isBestRate)
@ -303,15 +303,15 @@ internal class SelectProviderModel @Inject constructor(
* 1. Highest rate
* 2. Smallest difference between entered amount and required min/max amount
*/
private fun List<OnrampProviderWithQuote>.sortByRate() = sortedByDescending {
when (it) {
is OnrampProviderWithQuote.Data -> it.toAmount.value
private fun List<OnrampProviderWithQuote>.sortByRate() = sortedByDescending { quote ->
when (quote) {
is OnrampProviderWithQuote.Data -> quote.toAmount.value
// negative difference to sort both when data and unavailable is present
is OnrampProviderWithQuote.Unavailable.AmountError -> {
when (val error = it.quoteError.error) {
is OnrampError.AmountError.TooSmallError -> it.quoteError.fromAmount.value - error.requiredAmount
is OnrampError.AmountError.TooBigError -> error.requiredAmount - it.quoteError.fromAmount.value
when (val error = quote.quoteError.error) {
is OnrampError.AmountError.TooSmallError -> quote.quoteError.fromAmount.value - error.requiredAmount
is OnrampError.AmountError.TooBigError -> error.requiredAmount - quote.quoteError.fromAmount.value
else -> null
}
}

View file

@ -27,7 +27,7 @@ internal object SelectProviderPreviewData {
termsOfUseLink = null,
privacyPolicyLink = null,
),
paymentMethods = listOf(),
paymentMethods = emptyList(),
)
private val providerListUM = PaymentProviderUM(

View file

@ -83,14 +83,14 @@ internal class OnrampRedirectModel @Inject constructor(
isDarkTheme = isDarkTheme,
)
.onLeft(::handleError)
.onRight {
latestOnrampTransaction = it
.onRight { transaction ->
latestOnrampTransaction = transaction
// Workaround to open Unlimit provider in external browser instead of chrome custom tabs
if (params.onrampProviderWithQuote.provider.id.equals(UNLIMIT_PROVIDER_ID, ignoreCase = true)) {
urlOpener.openUrlExternalBrowser(it.redirectUrl)
urlOpener.openUrlExternalBrowser(transaction.redirectUrl)
} else {
urlOpener.openUrl(it.redirectUrl)
urlOpener.openUrl(transaction.redirectUrl)
}
}
}

View file

@ -47,6 +47,7 @@ internal class DefaultOnrampComponent @AssistedInject constructor(
childFactory = ::screenChild,
)
@Suppress("ReusedModifierInstance")
@Composable
override fun Content(modifier: Modifier) {
val childStack by contentStack.subscribeAsState()
@ -78,10 +79,10 @@ internal class DefaultOnrampComponent @AssistedInject constructor(
cryptoCurrency = params.cryptoCurrency,
openSettings = { navigation.push(OnrampChild.Settings) },
source = params.source,
openRedirectPage = {
openRedirectPage = { quote ->
navigation.push(
OnrampChild.RedirectPage(
quote = it,
quote = quote,
cryptoCurrency = params.cryptoCurrency,
),
)
@ -96,15 +97,15 @@ internal class DefaultOnrampComponent @AssistedInject constructor(
cryptoCurrency = params.cryptoCurrency,
openSettings = { navigation.push(OnrampChild.Settings) },
source = params.source,
openRedirectPage = {
openRedirectPage = { onrampProviderWithQuoteData ->
navigation.push(
OnrampChild.RedirectPage(
quote = it,
quote = onrampProviderWithQuoteData,
cryptoCurrency = params.cryptoCurrency,
),
)
},
launchSepa = params.shouldLaunchSepa,
isLaunchSepa = params.shouldLaunchSepa,
),
)
}

View file

@ -6,7 +6,6 @@ import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
internal class CountryListUMController(
@ -14,16 +13,17 @@ internal class CountryListUMController(
loadingItems: ImmutableList<CountryItemState.Loading>,
) {
val state: StateFlow<CountryListUM> get() = _state.asStateFlow()
private val _state: MutableStateFlow<CountryListUM> =
MutableStateFlow(value = CountryListUM.Loading(searchBarUM = searchBarUM, items = loadingItems))
val state: StateFlow<CountryListUM>
field = MutableStateFlow<CountryListUM>(
value = CountryListUM.Loading(searchBarUM = searchBarUM, items = loadingItems),
)
fun update(transformer: Transformer<CountryListUM>) {
_state.update(transformer::transform)
state.update(transformer::transform)
}
fun update(transformer: SearchBarUMTransformer) {
_state.update { prevState ->
state.update { prevState ->
val searchBarUM = transformer.transform(prevState.searchBarUM)
prevState.copySealed(searchBarUM)
}

View file

@ -52,13 +52,13 @@ internal class OnrampSelectCountryModel @Inject constructor(
private val params: SelectCountryComponent.Params = paramsContainer.require()
private val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
val state: StateFlow<CountryListUM> get() = controller.state
private val controller = CountryListUMController(
searchBarUM = createSearchBarUM(),
loadingItems = loadingItems,
)
val state: StateFlow<CountryListUM> get() = controller.state
init {
analyticsEventHandler.send(OnrampAnalyticsEvent.SelectResidenceOpened)
updateCountriesList()
@ -69,7 +69,7 @@ internal class OnrampSelectCountryModel @Inject constructor(
params.onDismiss(isCountrySelected)
}
private suspend fun subscribeOnUpdateState() {
private fun subscribeOnUpdateState() {
combine(
flow = getOnrampCountriesUseCase(),
flow2 = getOnrampCountryUseCase(),

View file

@ -6,27 +6,26 @@ import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
internal class CurrencyListController(
currencySearchBarUM: SearchBarUM,
loadingSections: ImmutableList<CurrenciesSection<CurrencyItemState.Loading>>,
) {
val state: StateFlow<CurrenciesListUM> get() = _state.asStateFlow()
private val _state: MutableStateFlow<CurrenciesListUM> = MutableStateFlow(
value = CurrenciesListUM.Loading(
searchBarUM = currencySearchBarUM,
sections = loadingSections,
),
)
val state: StateFlow<CurrenciesListUM>
field = MutableStateFlow<CurrenciesListUM>(
value = CurrenciesListUM.Loading(
searchBarUM = currencySearchBarUM,
sections = loadingSections,
),
)
fun update(transformer: Transformer<CurrenciesListUM>) {
_state.update(transformer::transform)
state.update(transformer::transform)
}
fun update(transformer: SearchBarUMTransformer) {
_state.update { prevState ->
state.update { prevState ->
val searchBarUM = transformer.transform(prevState.searchBarUM)
prevState.copySealed(searchBarUM)
}

View file

@ -46,14 +46,14 @@ internal class OnrampSelectCurrencyModel @Inject constructor(
paramsContainer: ParamsContainer,
) : Model() {
val state: StateFlow<CurrenciesListUM> get() = controller.state
private val params: SelectCurrencyComponent.Params = paramsContainer.require()
private val controller = CurrencyListController(
currencySearchBarUM = createSearchBarUM(),
loadingSections = loadingSections,
)
val state: StateFlow<CurrenciesListUM> get() = controller.state
private val params: SelectCurrencyComponent.Params = paramsContainer.require()
init {
updateCurrenciesList()
subscribeOnUpdateState()

View file

@ -49,12 +49,11 @@ internal class OnrampOperationModel @Inject constructor(
private val rampStateManager: RampStateManager,
) : Model() {
val state: StateFlow<OnrampOperationUM> get() = _state
val state: StateFlow<OnrampOperationUM>
field = MutableStateFlow(value = getInitialState())
private val params: Params = paramsContainer.require()
private val _state = MutableStateFlow(value = getInitialState())
private val selectedUserWallet = getWalletsUseCase.invokeSync()
.first { it.walletId == params.userWalletId }
@ -176,7 +175,7 @@ internal class OnrampOperationModel @Inject constructor(
fun onTokenListInitialized() {
// Makes HotCrypto tokens visible when token list is initialized to synchronize UI
_state.update {
state.update {
it.copy(isHotCryptoVisible = true)
}
}

View file

@ -29,11 +29,12 @@ internal class OnrampSettingsModel @Inject constructor(
paramsContainer: ParamsContainer,
) : Model() {
val state: StateFlow<OnrampSettingsUM> get() = _state
val state: StateFlow<OnrampSettingsUM>
field = MutableStateFlow(getInitialState())
val bottomSheetNavigation: SlotNavigation<OnrampSettingsConfig> = SlotNavigation()
private val params: OnrampSettingsComponent.Params = paramsContainer.require()
private val _state: MutableStateFlow<OnrampSettingsUM> = MutableStateFlow(getInitialState())
init {
analyticsEventHandler.send(OnrampAnalyticsEvent.SettingsOpened)
@ -52,7 +53,7 @@ internal class OnrampSettingsModel @Inject constructor(
analyticsEventHandler.sendOnrampErrorEvent(it, params.cryptoCurrency.symbol)
},
ifRight = { country ->
_state.update { state ->
state.update { state ->
state.copy(
items = listOf(
OnrampSettingsItemUM.Residence(

View file

@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.GetOnrampStatusUseCase
import com.tangem.domain.onramp.GetOnrampTransactionUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
@ -21,7 +22,6 @@ import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.onramp.component.OnrampSuccessComponent
import com.tangem.features.onramp.impl.R
@ -32,6 +32,7 @@ import com.tangem.features.onramp.utils.sendOnrampErrorEvent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.PeriodicTask
import com.tangem.utils.coroutines.SingleTaskScheduler
import com.tangem.utils.coroutines.runSuspendCatching
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -66,7 +67,7 @@ internal class OnrampSuccessComponentModel @Inject constructor(
private var userWallet: UserWallet by Delegates.notNull()
private var cryptoCurrency: CryptoCurrency by Delegates.notNull()
private var expressTxStatusTaskScheduler = SingleTaskScheduler<Unit>()
private val expressTxStatusTaskScheduler = SingleTaskScheduler<Unit>()
init {
loadData()
@ -119,7 +120,7 @@ internal class OnrampSuccessComponentModel @Inject constructor(
isDelayFirst = false,
delay = EXPRESS_STATUS_UPDATE_DELAY,
task = {
runCatching {
runSuspendCatching {
loadTransactionStatus(transaction)
}
},

View file

@ -44,6 +44,7 @@ import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runSuspendCatching
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -68,7 +69,7 @@ internal class AvailableSwapPairsModel @Inject constructor(
val state: StateFlow<TokenListUM> = tokenListUMController.state
private var params: AvailableSwapPairsComponent.Params = paramsContainer.require()
private val params: AvailableSwapPairsComponent.Params = paramsContainer.require()
private val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
private val tokenListFlow = getTokenListUseCaseFlow()
@ -140,9 +141,9 @@ internal class AvailableSwapPairsModel @Inject constructor(
availablePairs = pairs,
)
},
ifError = {
ifError = { throwable ->
handleErrorState(
cause = it,
cause = throwable,
networkInfo = params.selectedStatus.value?.toLeastTokenInfo(),
currencies = currencies,
)
@ -184,9 +185,9 @@ internal class AvailableSwapPairsModel @Inject constructor(
isAccountsMode = isAccountsMode,
)
},
ifError = {
ifError = { throwable ->
handleErrorStateV2(
cause = it,
cause = throwable,
networkInfo = params.selectedStatus.value?.toLeastTokenInfo(),
accountList = accountList,
)
@ -218,7 +219,7 @@ internal class AvailableSwapPairsModel @Inject constructor(
isBalanceHidden = isBalanceHidden,
unavailableTokensHeaderReference = resourceReference(
id = R.string.tokens_list_unavailable_to_swap_header,
wrappedList(selectedStatus?.currency?.name?.capitalize() ?: ""),
wrappedList(selectedStatus?.currency?.name?.capitalize().orEmpty()),
),
)
}
@ -242,7 +243,7 @@ internal class AvailableSwapPairsModel @Inject constructor(
isBalanceHidden = isBalanceHidden,
unavailableTokensHeaderReference = resourceReference(
id = R.string.tokens_list_unavailable_to_swap_header,
wrappedList(selectedStatus?.currency?.name?.capitalize() ?: ""),
wrappedList(selectedStatus?.currency?.name?.capitalize().orEmpty()),
),
)
}
@ -369,7 +370,7 @@ internal class AvailableSwapPairsModel @Inject constructor(
}
private suspend fun updateAvailablePairs(networkInfo: LeastTokenInfo, statuses: List<CryptoCurrencyStatus>) {
runCatching {
runSuspendCatching {
availablePairsByNetworkFlow.update(networkInfo = networkInfo, state = lceLoading())
getAvailablePairsUseCase(
@ -390,9 +391,9 @@ internal class AvailableSwapPairsModel @Inject constructor(
networkInfo: LeastTokenInfo,
state: AvailablePairsState,
) {
update {
it.toMutableMap().apply {
put(networkInfo, state)
update { map ->
map.toMutableMap().apply {
this[networkInfo] = state
}
}
}
@ -433,9 +434,9 @@ internal class AvailableSwapPairsModel @Inject constructor(
}
private fun List<CryptoCurrencyStatus>.filterByQuery(query: String): List<CryptoCurrencyStatus> {
return filter {
it.currency.name.contains(other = query, ignoreCase = true) ||
it.currency.symbol.contains(other = query, ignoreCase = true)
return filter { status ->
status.currency.name.contains(other = query, ignoreCase = true) ||
status.currency.symbol.contains(other = query, ignoreCase = true)
}
}

View file

@ -15,24 +15,23 @@ import javax.inject.Inject
*/
internal class SwapSelectTokensController @Inject constructor() {
val state: StateFlow<SwapSelectTokensUM> get() = _state
private val _state: MutableStateFlow<SwapSelectTokensUM> = MutableStateFlow(
value = SwapSelectTokensUM(
onBackClick = {},
exchangeFrom = createEmptyExchangeFrom(),
exchangeTo = createEmptyExchangeTo(),
isBalanceHidden = false,
),
)
val state: StateFlow<SwapSelectTokensUM>
field = MutableStateFlow(
value = SwapSelectTokensUM(
onBackClick = {},
exchangeFrom = createEmptyExchangeFrom(),
exchangeTo = createEmptyExchangeTo(),
isBalanceHidden = false,
),
)
fun update(transform: (SwapSelectTokensUM) -> SwapSelectTokensUM) {
Timber.d("Applying non-name transformation")
_state.update(transform)
state.update(transform)
}
fun update(transformer: SwapSelectTokensUMTransformer) {
Timber.d("Applying ${transformer::class.simpleName}")
_state.update(transformer::transform)
Timber.d("Applying ${transformer::class.simpleName ?: "null"}")
state.update(transformer::transform)
}
}

View file

@ -12,7 +12,6 @@ import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.onramp.component.SwapSelectTokensComponent
import com.tangem.features.onramp.swap.entity.SwapSelectTokensController
@ -43,15 +42,14 @@ internal class SwapSelectTokensModel @Inject constructor(
val state: StateFlow<SwapSelectTokensUM> = controller.state
val fromCurrencyStatus: StateFlow<CryptoCurrencyStatus?> get() = _fromCurrencyStatus
val fromCurrencyStatus: StateFlow<CryptoCurrencyStatus?>
field = MutableStateFlow<CryptoCurrencyStatus?>(value = null)
private val _fromCurrencyStatus = MutableStateFlow<CryptoCurrencyStatus?>(value = null)
private val _toCurrencyStatus = MutableStateFlow<CryptoCurrencyStatus?>(value = null)
private val params = paramsContainer.require<SwapSelectTokensComponent.Params>()
private var isAccountsMode: Boolean = false
private var account: Account.CryptoPortfolio? = null
init {
controller.update { it.copy(onBackClick = ::onBackClick) }
@ -71,7 +69,7 @@ internal class SwapSelectTokensModel @Inject constructor(
event = MainScreenAnalyticsEvent.SwapTokenClicked(currencySymbol = status.currency.symbol),
)
_fromCurrencyStatus.value = status
fromCurrencyStatus.value = status
modelScope.launch {
controller.update(
@ -173,7 +171,7 @@ internal class SwapSelectTokensModel @Inject constructor(
}
private fun onRemoveFromTokenClick() {
val currencySymbol = requireNotNull(_fromCurrencyStatus.value?.currency?.symbol) {
val currencySymbol = requireNotNull(fromCurrencyStatus.value?.currency?.symbol) {
"Token was not selected"
}
@ -185,7 +183,7 @@ internal class SwapSelectTokensModel @Inject constructor(
}
private fun removeSelectedFromToken() {
_fromCurrencyStatus.value = null
fromCurrencyStatus.value = null
controller.update(transformer = RemoveSelectedFromTokenTransformer)
}

View file

@ -18,37 +18,36 @@ import javax.inject.Inject
*/
internal class TokenListUMController @Inject constructor() {
val state: StateFlow<TokenListUM> get() = _state
private val _state: MutableStateFlow<TokenListUM> = MutableStateFlow(
value = TokenListUM(
searchBarUM = SearchBarUM(
placeholderText = resourceReference(id = R.string.common_search),
query = "",
onQueryChange = {},
isActive = false,
onActiveChange = {},
val state: StateFlow<TokenListUM>
field = MutableStateFlow(
value = TokenListUM(
searchBarUM = SearchBarUM(
placeholderText = resourceReference(id = R.string.common_search),
query = "",
onQueryChange = {},
isActive = false,
onActiveChange = {},
),
availableItems = persistentListOf(),
unavailableItems = persistentListOf(),
tokensListData = TokenListUMData.EmptyList,
isBalanceHidden = false,
),
availableItems = persistentListOf(),
unavailableItems = persistentListOf(),
tokensListData = TokenListUMData.EmptyList,
isBalanceHidden = false,
),
)
)
fun update(transform: (TokenListUM) -> TokenListUM) {
Timber.d("Applying non-name transformation")
_state.update(transform)
state.update(transform)
}
fun update(transformer: TokenListUMTransformer) {
Timber.d("Applying ${transformer::class.simpleName}")
_state.update(transformer::transform)
Timber.d("Applying ${transformer::class.simpleName ?: "unknown"}")
state.update(transformer::transform)
}
fun update(transformer: SearchBarUMTransformer) {
Timber.d("Applying ${transformer::class.simpleName}")
_state.update { prevState ->
Timber.d("Applying ${transformer::class.simpleName ?: "unknown"}")
state.update { prevState ->
prevState.copy(
searchBarUM = transformer.transform(prevState.searchBarUM),
)

View file

@ -8,7 +8,6 @@ import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
internal class SetNothingToFoundStateTransformer(
private val isBalanceHidden: Boolean,
@ -17,17 +16,15 @@ internal class SetNothingToFoundStateTransformer(
override fun transform(prevState: TokenListUM): TokenListUM {
return prevState.copy(
availableItems = buildList {
availableItems = persistentListOf(
createGroupTitle(
textReference = resourceReference(id = R.string.exchange_tokens_available_tokens_header),
)
.let(::add)
),
TokensListItemUM.Text(
id = emptySearchMessageReference.hashCode(),
text = emptySearchMessageReference,
).let(::add)
}.toImmutableList(),
),
),
unavailableItems = persistentListOf(),
tokensListData = TokenListUMData.EmptyList,
isBalanceHidden = isBalanceHidden,

View file

@ -6,7 +6,6 @@ import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
internal class SetNothingToFoundStateTransformerV2(
private val isBalanceHidden: Boolean,
@ -17,12 +16,14 @@ internal class SetNothingToFoundStateTransformerV2(
return prevState.copy(
availableItems = persistentListOf(),
unavailableItems = persistentListOf(),
tokensListData = TokenListUMData.TokenList(tokensList = buildList {
TokensListItemUM.Text(
id = emptySearchMessageReference.hashCode(),
text = emptySearchMessageReference,
).let(::add)
}.toImmutableList()),
tokensListData = TokenListUMData.TokenList(
tokensList = persistentListOf(
TokensListItemUM.Text(
id = emptySearchMessageReference.hashCode(),
text = emptySearchMessageReference,
),
),
),
isBalanceHidden = isBalanceHidden,
)
}

View file

@ -22,11 +22,11 @@ internal object OnrampTokenItemStateConverterFactory {
): 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,
@ -41,16 +41,16 @@ internal object OnrampTokenItemStateConverterFactory {
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,
text = stringReference(value = it.currency.symbol),
status = status,
text = stringReference(value = status.currency.symbol),
isAvailable = false,
)
},
@ -68,15 +68,15 @@ internal object OnrampTokenItemStateConverterFactory {
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,
)

View file

@ -10,7 +10,7 @@ fun List<TokensListItemUM>.addHeader(textReference: TextReference): ImmutableLis
return buildList {
if (items.isNotEmpty()) {
createGroupTitle(textReference = textReference).let(::add)
add(createGroupTitle(textReference = textReference))
}
addAll(items)

View file

@ -5,6 +5,7 @@ import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.fields.InputManager
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.models.AccountStatusList
@ -120,11 +121,11 @@ internal class OnrampTokenListModel @Inject constructor(
UpdateTokenItemsTransformer(
appCurrency = appCurrency,
onItemClick = params.onTokenClick,
statuses = filterByQueryTokenList.let {
statuses = filterByQueryTokenList.let { statuses ->
if (hasRestrictionForSell || isInsufficientBalanceForSell) {
mapOf(false to it)
mapOf(false to statuses)
} else {
it.filterByAvailability()
statuses.filterByAvailability()
}
},
isBalanceHidden = isBalanceHidden,
@ -226,17 +227,25 @@ internal class OnrampTokenListModel @Inject constructor(
}
}
private fun getUnavailableTokensHeaderReference() = when (params.filterOperation) {
OnrampOperation.BUY -> R.string.tokens_list_unavailable_to_purchase_header
OnrampOperation.SELL -> R.string.tokens_list_unavailable_to_sell_header
OnrampOperation.SWAP -> R.string.tokens_list_unavailable_to_swap_source_header
}.let(::resourceReference)
private fun getUnavailableTokensHeaderReference(): TextReference {
val res = when (params.filterOperation) {
OnrampOperation.BUY -> R.string.tokens_list_unavailable_to_purchase_header
OnrampOperation.SELL -> R.string.tokens_list_unavailable_to_sell_header
OnrampOperation.SWAP -> R.string.tokens_list_unavailable_to_swap_source_header
}
private fun getEmptySearchMessageReference() = when (params.filterOperation) {
OnrampOperation.BUY -> R.string.action_buttons_buy_empty_search_message
OnrampOperation.SELL -> R.string.action_buttons_sell_empty_search_message
OnrampOperation.SWAP -> R.string.action_buttons_swap_empty_search_message
}.let(::resourceReference)
return resourceReference(res)
}
private fun getEmptySearchMessageReference(): TextReference {
val res = when (params.filterOperation) {
OnrampOperation.BUY -> R.string.action_buttons_buy_empty_search_message
OnrampOperation.SELL -> R.string.action_buttons_sell_empty_search_message
OnrampOperation.SWAP -> R.string.action_buttons_swap_empty_search_message
}
return resourceReference(res)
}
private fun updateTokenListUM(transformer: TokenListUMTransformer) {
modelScope.launch {
@ -297,9 +306,9 @@ internal class OnrampTokenListModel @Inject constructor(
}.filter { (_, value) -> value.isNotEmpty() }
private fun List<CryptoCurrencyStatus>.filterByQuery(query: String): List<CryptoCurrencyStatus> {
return filter {
it.currency.name.contains(other = query, ignoreCase = true) ||
it.currency.symbol.contains(other = query, ignoreCase = true)
return filter { status ->
status.currency.name.contains(other = query, ignoreCase = true) ||
status.currency.symbol.contains(other = query, ignoreCase = true)
}
}