Updated on 2026-08-14

This commit is contained in:
Tangem 2024-10-29 14:31:53 +03:00
commit 652159dbce
316 changed files with 6242 additions and 3084 deletions

View file

@ -16,4 +16,7 @@ dependencies {
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
/* Compose */
implementation(deps.compose.runtime)
}

View file

@ -13,4 +13,7 @@ dependencies {
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
/* Compose */
implementation(deps.compose.runtime)
}

View file

@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.update
internal class PreviewManageTokensComponent(
private val isLoading: Boolean,
private val showTangemIcon: Boolean,
params: ManageTokensComponent.Params,
) : ManageTokensComponent {
@ -65,6 +66,7 @@ internal class PreviewManageTokensComponent(
loadMore = { false },
saveChanges = {},
isSavingInProgress = false,
needToAddDerivations = showTangemIcon,
),
)

View file

@ -14,6 +14,7 @@ internal data class CustomTokenFormUM(
val notifications: PersistentList<NotificationUM> = persistentListOf(),
val canAddToken: Boolean = false,
val isValidating: Boolean = false,
val needToAddDerivation: Boolean = false,
val saveToken: () -> Unit,
) {

View file

@ -14,6 +14,4 @@ internal data class CurrencyNetworkUM(
) : SelectableItemUM {
override val id: String = network.id.value
data class LongTapConfig(val contractAddress: String, val onLongTap: () -> Unit)
}

View file

@ -42,6 +42,7 @@ internal sealed class ManageTokensUM {
val saveChanges: () -> Unit,
val hasChanges: Boolean,
val isSavingInProgress: Boolean,
val needToAddDerivations: Boolean,
) : ManageTokensUM()
fun copySealed(
@ -52,6 +53,7 @@ internal sealed class ManageTokensUM {
isNextBatchLoading: Boolean = this.isNextBatchLoading,
isSavingInProgress: Boolean = this is ManageContent && this.isSavingInProgress,
scrollToTop: StateEvent<Unit> = this.scrollToTop,
needToAddDerivations: Boolean = this is ManageContent && this.needToAddDerivations,
): ManageTokensUM {
return when (this) {
is ManageContent -> copy(
@ -62,6 +64,7 @@ internal sealed class ManageTokensUM {
isNextBatchLoading = isNextBatchLoading,
isSavingInProgress = isSavingInProgress,
scrollToTop = scrollToTop,
needToAddDerivations = needToAddDerivations,
)
is ReadContent -> copy(
search = search,

View file

@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.ContentMessage
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.card.HasMissedDerivationsUseCase
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
@ -43,6 +44,7 @@ internal class CustomTokenFormModel @Inject constructor(
private val customCurrencyValidator: CustomCurrencyValidator,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
private val messageSender: UiMessageSender,
private val customTokenFormManager: CustomCurrencyFormBuilder,
private val analyticsEventHandler: AnalyticsEventHandler,
@ -159,7 +161,12 @@ internal class CustomTokenFormModel @Inject constructor(
fillForm: Boolean,
isAlreadyAdded: Boolean,
isCustom: Boolean,
) {
) = modelScope.launch {
val needToAddDerivation = hasMissedDerivationsUseCase(
userWalletId = params.userWalletId,
networksWithDerivationPath = mapOf(currency.network.backendId to getDerivationPath().value),
)
state.update { state ->
var updatedState = state
.updateWithProgress(
@ -169,6 +176,7 @@ internal class CustomTokenFormModel @Inject constructor(
clearNotifications = true,
clearFieldErrors = true,
disableSecondaryFields = !isCustom,
needToAddDerivation = needToAddDerivation,
)
if (fillForm) {

View file

@ -17,6 +17,7 @@ import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.card.HasMissedDerivationsUseCase
import com.tangem.domain.managetokens.SaveManagedTokensUseCase
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
@ -47,6 +48,7 @@ internal class ManageTokensModel @Inject constructor(
private val router: Router,
private val manageTokensListManager: ManageTokensListManager,
private val messageSender: UiMessageSender,
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
paramsContainer: ParamsContainer,
@ -140,6 +142,7 @@ internal class ManageTokensModel @Inject constructor(
hasChanges = false,
saveChanges = ::saveChanges,
loadMore = ::loadMoreItems,
needToAddDerivations = false,
isSavingInProgress = false,
)
}
@ -255,10 +258,22 @@ internal class ManageTokensModel @Inject constructor(
}
private fun updateChangedItems(currenciesToAdd: ChangedCurrencies, currenciesToRemove: ChangedCurrencies) {
state.update { state ->
state.copySealed(
hasChanges = currenciesToAdd.isNotEmpty() || currenciesToRemove.isNotEmpty(),
)
modelScope.launch {
val hasMissedDerivations = params.userWalletId?.let { walletId ->
val networks = currenciesToAdd.values
.flatten()
.toSet()
.associate { it.backendId to null }
hasMissedDerivationsUseCase(walletId, networks)
}
state.update { state ->
state.copySealed(
hasChanges = currenciesToAdd.isNotEmpty() || currenciesToRemove.isNotEmpty(),
needToAddDerivations = hasMissedDerivations ?: false,
)
}
}
}

View file

@ -61,9 +61,11 @@ internal class OnboardingManageTokensModel @Inject constructor(
.onEach { status -> updatePaginationStatus(status) }
.launchIn(modelScope)
manageTokensListManager.currenciesToAdd
.onEach(::handleNewAddedCurrencies)
.launchIn(modelScope)
combine(
manageTokensListManager.currenciesToAdd,
manageTokensListManager.currenciesToRemove,
::handleChangedCurrencies,
).launchIn(modelScope)
observeSearchQueryChanges()
@ -192,8 +194,11 @@ internal class OnboardingManageTokensModel @Inject constructor(
state.update { state -> state.copy(scrollToTop = consumedEvent()) }
}
private suspend fun handleNewAddedCurrencies(currenciesToAdd: ChangedCurrencies) {
if (currenciesToAdd.isEmpty()) {
private suspend fun handleChangedCurrencies(
currenciesToAdd: ChangedCurrencies,
currenciesToRemove: ChangedCurrencies,
) {
if (currenciesToAdd.isEmpty() && currenciesToRemove.isEmpty()) {
state.update { state ->
state.copy(
actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Later(onClick = ::onLaterClick),

View file

@ -22,9 +22,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.bottomFade
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.fields.SimpleTextField
import com.tangem.core.ui.components.isOpened
import com.tangem.core.ui.components.keyboardAsState
@ -82,14 +84,22 @@ internal fun CustomTokenFormContent(model: CustomTokenFormUM, modifier: Modifier
}
}
PrimaryButton(
TangemButton(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = TangemTheme.dimens.spacing16 + bottomBarHeight)
.fillMaxWidth(),
text = stringResource(id = R.string.custom_token_add_token),
colors = TangemButtonsDefaults.primaryButtonColors,
enabled = model.canAddToken,
showProgress = model.isValidating,
animateContentChange = true,
icon = if (model.needToAddDerivation) {
TangemButtonIconPosition.End(R.drawable.ic_tangem_24)
} else {
TangemButtonIconPosition.None
},
textStyle = TangemTheme.typography.subtitle1,
onClick = model.saveToken,
)
}

View file

@ -285,6 +285,18 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider :
onDerivationPathSelected = {},
),
),
PreviewCustomTokenSelectorComponent(
params = CustomTokenSelectorComponent.Params.NetworkSelector(
userWalletId = UserWalletId(stringValue = "321"),
selectedNetwork = SelectedNetwork(
id = Network.ID(value = "0"),
name = "Ethereum",
derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0"),
canHandleTokens = true,
),
onNetworkSelected = {},
),
),
)
}
// endregion Preview

View file

@ -33,11 +33,17 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.TangemSwitch
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.fields.SearchBar
@ -122,6 +128,7 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
.fillMaxWidth(),
isVisible = state.hasChanges,
showProgress = state.isSavingInProgress,
showIcon = state.needToAddDerivations,
onClick = state.saveChanges,
)
}
@ -158,6 +165,7 @@ private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM?, search: SearchBarU
private fun SaveChangesButton(
isVisible: Boolean,
showProgress: Boolean,
showIcon: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
@ -168,10 +176,18 @@ private fun SaveChangesButton(
exit = fadeOut(),
label = "save_button_visibility",
) {
PrimaryButtonIconEnd(
TangemButton(
text = stringResource(id = R.string.common_save),
iconResId = R.drawable.ic_tangem_24,
icon = if (showIcon) {
TangemButtonIconPosition.End(R.drawable.ic_tangem_24)
} else {
TangemButtonIconPosition.None
},
showProgress = showProgress,
colors = TangemButtonsDefaults.primaryButtonColors,
textStyle = TangemTheme.typography.subtitle1,
enabled = true,
animateContentChange = true,
onClick = onClick,
)
}
@ -472,6 +488,7 @@ private class PreviewManageTokensComponentProvider : PreviewParameterProvider<Ma
get() = sequenceOf(
PreviewManageTokensComponent(
isLoading = true,
showTangemIcon = true,
params = ManageTokensComponent.Params(
source = ManageTokensSource.ONBOARDING,
userWalletId = UserWalletId("wallet_id"),
@ -479,10 +496,12 @@ private class PreviewManageTokensComponentProvider : PreviewParameterProvider<Ma
),
PreviewManageTokensComponent(
isLoading = false,
showTangemIcon = true,
params = ManageTokensComponent.Params(source = ManageTokensSource.ONBOARDING, userWalletId = null),
),
PreviewManageTokensComponent(
isLoading = false,
showTangemIcon = false,
params = ManageTokensComponent.Params(
source = ManageTokensSource.ONBOARDING,
userWalletId = UserWalletId("wallet_id"),

View file

@ -68,6 +68,7 @@ internal class ManageTokensListManager @Inject constructor(
dispatchers = dispatchers,
actions = this,
scopeProvider = Provider { scope },
sourceProvider = Provider { source },
clipboardManager = clipboardManager,
)

View file

@ -10,6 +10,7 @@ import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.ui.dialog.CurrencyUnsupportedDialog
@ -29,11 +30,13 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@Suppress("LongParameterList")
internal class ManageTokensUiManager(
private val state: MutableStateFlow<ManageTokensListState>,
private val messageSender: UiMessageSender,
private val dispatchers: CoroutineDispatcherProvider,
private val scopeProvider: Provider<CoroutineScope>,
private val sourceProvider: Provider<ManageTokensSource>,
private val actions: ManageTokensUiActions,
private val clipboardManager: ClipboardManager,
) {
@ -41,6 +44,9 @@ internal class ManageTokensUiManager(
private val scope: CoroutineScope
get() = scopeProvider()
private val source: ManageTokensSource
get() = sourceProvider()
@OptIn(ExperimentalCoroutinesApi::class)
val items: Flow<ImmutableList<CurrencyItemUM>> = state
.mapLatest { state ->
@ -225,26 +231,39 @@ internal class ManageTokensUiManager(
} else {
actions.checkHasLinkedTokens(userWalletId, network)
}
val canHideWithoutConfirming = source == ManageTokensSource.ONBOARDING
val message = ContentMessage { onDismiss ->
if (hasLinkedTokens) {
HasLinkedTokensWarning(
currency = currency,
network = network,
onDismiss = onDismiss,
)
} else {
HideTokenWarning(
currency = currency,
onConfirm = {
onConfirm()
onDismiss()
},
onDismiss = onDismiss,
)
}
if (hasLinkedTokens) {
showLinkedTokensWarning(currency, network)
} else if (canHideWithoutConfirming) {
onConfirm()
} else {
showHideTokenWarning(currency, onConfirm)
}
}
private fun showLinkedTokensWarning(currency: ManagedCryptoCurrency, network: Network) {
val message = ContentMessage { onDismiss ->
HasLinkedTokensWarning(
currency = currency,
network = network,
onDismiss = onDismiss,
)
}
messageSender.send(message)
}
private fun showHideTokenWarning(currency: ManagedCryptoCurrency, onConfirm: () -> Unit) {
val message = ContentMessage { onDismiss ->
HideTokenWarning(
currency = currency,
onConfirm = {
onConfirm()
onDismiss()
},
onDismiss = onDismiss,
)
}
messageSender.send(message)
}

View file

@ -27,6 +27,7 @@ internal fun CustomTokenFormUM.updateWithProgress(
showProgress: Boolean,
isWasFilled: Boolean = this.tokenForm?.wasFilled ?: false,
canAddToken: Boolean = this.canAddToken,
needToAddDerivation: Boolean = false,
clearNotifications: Boolean = false,
clearFieldErrors: Boolean = false,
disableSecondaryFields: Boolean = false,
@ -34,6 +35,7 @@ internal fun CustomTokenFormUM.updateWithProgress(
return copy(
isValidating = showProgress,
canAddToken = canAddToken,
needToAddDerivation = needToAddDerivation,
notifications = if (clearNotifications) persistentListOf() else notifications,
).updateTokenForm {
val updatedFields = fields.mapValues { (key, field) ->

View file

@ -1,5 +0,0 @@
package com.tangem.features.markets
interface MarketsFeatureToggles {
val isFeatureEnabled: Boolean
}

View file

@ -1,11 +0,0 @@
package com.tangem.features.markets
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
internal class DefaultMarketsFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : MarketsFeatureToggles {
override val isFeatureEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("MARKETS_ENABLED")
}

View file

@ -17,6 +17,8 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
@ -172,12 +174,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
fiatCurrencySymbol = currentAppCurrency.value.symbol,
),
dateTimeText = resourceReference(R.string.common_today),
priceChangePercentText = params.token.tokenQuotes.h24Percent?.let {
BigDecimalFormatter.formatPercent(
percent = it,
useAbsoluteValue = true,
)
},
priceChangePercentText = params.token.tokenQuotes.h24Percent?.format { percent() },
priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(),
iconUrl = params.token.imageUrl,
chartState = MarketsTokenDetailsUM.ChartState(
@ -472,12 +469,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
)
} ?: currentQuotes.value.getPercentByInterval(currentState.selectedInterval)
val percentText = percent?.let {
BigDecimalFormatter.formatPercent(
percent = it,
useAbsoluteValue = true,
)
} ?: ""
val percentText = percent?.format { percent() } ?: ""
state.update { stateToUpdate ->
stateToUpdate.copy(

View file

@ -3,7 +3,10 @@ package com.tangem.features.markets.details.impl.model.converters
import androidx.compose.runtime.Stable
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.format.bigdecimal.compact
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.rawCompact
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarketInfo
@ -142,14 +145,17 @@ internal class InsightsConverter(
private fun BigDecimal.convertChange(isFiatValue: Boolean = false): String {
val value = if (isFiatValue) {
val currency = appCurrency()
BigDecimalFormatter.formatCompactFiatAmount(
amount = this.abs(),
fiatCurrencyCode = currency.code,
fiatCurrencySymbol = currency.symbol,
)
this.abs().format {
val currency = appCurrency()
fiat(
fiatCurrencyCode = currency.code,
fiatCurrencySymbol = currency.symbol,
).compact()
}
} else {
BigDecimalFormatter.formatCompactAmount(amount = this.abs())
this.abs().format {
rawCompact()
}
}
return when {

View file

@ -2,7 +2,10 @@ package com.tangem.features.markets.details.impl.model.converters
import androidx.compose.runtime.Stable
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.format.bigdecimal.compact
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
@ -129,18 +132,21 @@ internal class MetricsConverter(
if (this == null) return StringsSigns.DASH_SIGN
return if (crypto) {
BigDecimalFormatter.formatCompactCryptoAmount(
amount = this,
cryptoCurrencySymbol = tokenSymbol,
)
format {
crypto(
symbol = tokenSymbol,
decimals = 2,
).compact()
}
} else {
val currency = appCurrency()
BigDecimalFormatter.formatCompactFiatAmount(
amount = this,
fiatCurrencyCode = currency.code,
fiatCurrencySymbol = currency.symbol,
)
format {
fiat(
fiatCurrencyCode = currency.code,
fiatCurrencySymbol = currency.symbol,
).compact()
}
}
}
}

View file

@ -2,6 +2,8 @@ package com.tangem.features.markets.details.impl.model.formatter
import com.tangem.common.ui.charts.state.MarketChartLook
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.PriceChangeInterval
@ -28,12 +30,7 @@ internal fun TokenQuotes.getFormattedPercentByInterval(interval: PriceChangeInte
PriceChangeInterval.ALL_TIME -> allTimeChangePercent
}
return percent?.let {
BigDecimalFormatter.formatPercent(
percent = it,
useAbsoluteValue = true,
)
} ?: ""
return percent?.format { percent() } ?: ""
}
internal fun TokenQuotes.getPercentByInterval(interval: PriceChangeInterval): BigDecimal? {

View file

@ -8,12 +8,14 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SmallButtonShimmer
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.information.InformationBlock
@ -76,10 +78,26 @@ private fun Title() {
private fun AddButton(state: AddButtonState, onClick: () -> Unit) {
when (state) {
AddButtonState.Loading -> {
SmallButtonShimmer(
modifier = Modifier.size(width = TangemTheme.dimens.size63, height = TangemTheme.dimens.size18),
shape = RoundedCornerShape(TangemTheme.dimens.radius3),
)
Box {
SmallButtonShimmer(
modifier = Modifier.width(width = TangemTheme.dimens.size63),
shape = RoundedCornerShape(TangemTheme.dimens.radius3),
withIcon = true,
)
Box(
Modifier
.matchParentSize()
.background(TangemTheme.colors.background.action),
)
RectangleShimmer(
modifier = Modifier
.align(Alignment.Center)
.size(width = TangemTheme.dimens.size63, height = TangemTheme.dimens.size18),
radius = TangemTheme.dimens.radius3,
)
}
}
AddButtonState.Available,
AddButtonState.Unavailable,

View file

@ -11,6 +11,8 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
@ -91,10 +93,7 @@ internal class TokenMarketBlockModel @Inject constructor(
// TODO get currency from quotes use case [REDACTED_TASK_KEY]
fiatCurrencySymbol = currentAppCurrency.value.symbol,
),
h24Percent = BigDecimalFormatter.formatPercent(
percent = res.priceChange,
useAbsoluteValue = true,
),
h24Percent = res.priceChange.format { percent() },
priceChangeType = PriceChangeType.fromBigDecimal(res.priceChange),
)
}

View file

@ -5,6 +5,10 @@ import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
import com.tangem.common.ui.charts.state.sorted
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.format.bigdecimal.compact
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarket
@ -71,12 +75,14 @@ internal class MarketsTokenItemConverter(
private fun TokenMarket.getMarketCap(): String? {
val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null
return BigDecimalFormatter.formatCompactFiatAmount(
amount = value,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
threeDigitsMethod = true,
)
return value.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
).compact(
threeDigitsMethod = true,
)
}
}
private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price {
@ -144,9 +150,6 @@ internal class MarketsTokenItemConverter(
TrendInterval.M1 -> tokenQuotesShort.monthChangePercent
}
return BigDecimalFormatter.formatPercent(
percent = percent,
useAbsoluteValue = true,
)
return percent.format { percent() }
}
}

1
features/onboarding-v2/api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,22 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.features.onboarding.v2.api"
}
dependencies {
/* Project - Domain */
implementation(projects.domain.wallets.models)
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
/* Compose */
implementation(deps.compose.runtime)
}

View file

@ -0,0 +1,5 @@
package com.tangem.features.onboarding.v2
interface OnboardingV2FeatureToggles {
val isRewrittenOnboardingCodeInUse: Boolean
}

View file

@ -0,0 +1,13 @@
package com.tangem.features.onboarding.v2.entry
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface OnboardingEntryComponent : ComposableContentComponent {
data class Params(
val todo: String,
)
interface Factory : ComponentFactory<Params, OnboardingEntryComponent>
}

View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,58 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.features.onboarding.v2.impl"
}
dependencies {
/** Api */
implementation(projects.features.onboardingV2.api)
/** Core modules */
implementation(projects.core.featuretoggles)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.utils)
implementation(projects.core.ui)
implementation(projects.core.res)
implementation(projects.core.decompose)
/** Domain */
implementation(projects.domain.models)
implementation(projects.domain.feedback)
implementation(projects.domain.core)
/** Tangem libraries */
implementation(deps.tangem.card.core)
implementation(deps.tangem.card.android) {
exclude(module = "joda-time")
}
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)
implementation(deps.lifecycle.runtime.ktx)
/** Compose libraries */
implementation(deps.compose.material3)
implementation(deps.compose.animation)
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.coil)
/** Other libraries */
implementation(deps.kotlin.immutable.collections)
implementation(deps.kotlin.serialization)
implementation(deps.timber)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.onboarding.v2
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import javax.inject.Inject
internal class DefaultOnboardingV2FeatureToggles @Inject constructor(
private val featureTogglesManager: FeatureTogglesManager,
) : OnboardingV2FeatureToggles {
override val isRewrittenOnboardingCodeInUse: Boolean
get() = featureTogglesManager.isFeatureEnabled("ONBOARDING_CODE_REFACTORING_ENABLED")
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.onboarding.v2.di
import com.tangem.features.onboarding.v2.DefaultOnboardingV2FeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface FeatureModule {
@Singleton
@Binds
fun provideFeatureToggles(featureToggles: DefaultOnboardingV2FeatureToggles): OnboardingV2FeatureToggles
}

View file

@ -0,0 +1,31 @@
package com.tangem.features.onboarding.v2.stepper.api
import androidx.annotation.IntRange
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.scan.ScanResponse
import kotlinx.coroutines.flow.StateFlow
interface OnboardingStepperComponent : ComposableContentComponent {
data class StepperState(
@IntRange(from = 0) val currentStep: Int,
@IntRange(from = 0) val steps: Int,
val title: TextReference,
)
class Params(
val initState: StepperState,
val popBack: () -> Unit,
val scanResponse: ScanResponse,
)
val state: StateFlow<StepperState>
fun changeStep(step: Int)
fun nextStep()
fun changeTitle(title: TextReference)
interface Factory : ComponentFactory<Params, OnboardingStepperComponent>
}

View file

@ -0,0 +1,76 @@
package com.tangem.features.onboarding.v2.stepper.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent
import com.tangem.features.onboarding.v2.stepper.impl.ui.OnboardingStepper
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
internal class DefaultOnboardingStepperComponent @AssistedInject constructor(
@Assisted val context: AppComponentContext,
@Assisted val params: OnboardingStepperComponent.Params,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
) : OnboardingStepperComponent, AppComponentContext by context {
override val state = instanceKeeper.getOrCreateSimple { MutableStateFlow(params.initState) }
override fun changeStep(step: Int) {
state.update { it.copy(currentStep = step.coerceAtLeast(minimumValue = 0)) }
}
override fun nextStep() {
state.update { state ->
if (state.currentStep < state.steps - 1) {
state.copy(currentStep = state.currentStep + 1)
} else {
state
}
}
}
override fun changeTitle(title: TextReference) {
state.update { it.copy(title = title) }
}
private fun openSupport() {
componentScope.launch {
val cardInfo = getCardInfoUseCase(params.scanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo))
}
}
@Composable
override fun Content(modifier: Modifier) {
val state by state.collectAsStateWithLifecycle()
OnboardingStepper(
state = state,
onBackClick = remember(this) { params.popBack },
onSupportButtonClick = remember(this) { ::openSupport },
modifier = modifier,
)
}
@AssistedFactory
interface Factory : OnboardingStepperComponent.Factory {
override fun create(
context: AppComponentContext,
params: OnboardingStepperComponent.Params,
): DefaultOnboardingStepperComponent
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.onboarding.v2.stepper.impl.di
import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent
import com.tangem.features.onboarding.v2.stepper.impl.DefaultOnboardingStepperComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ComponentModule {
@Binds
@Singleton
fun bindOnboardingStepperComponent(
factory: DefaultOnboardingStepperComponent.Factory,
): OnboardingStepperComponent.Factory
}

View file

@ -0,0 +1,83 @@
package com.tangem.features.onboarding.v2.stepper.impl.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.progressbar.LinearProgressIndicator
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemAnimations
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent
@Composable
internal fun OnboardingStepper(
state: OnboardingStepperComponent.StepperState,
onBackClick: () -> Unit,
onSupportButtonClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val fraction = state.currentStep.toFloat() / state.steps.coerceAtLeast(1)
val animatedIndicatorFraction by TangemAnimations.horizontalIndicatorAsState(targetFraction = fraction)
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
TangemTopAppBar(
startButton = TopAppBarButtonUM.Back(onBackClick),
endButton = TopAppBarButtonUM(iconRes = R.drawable.ic_chat_24, onIconClicked = onSupportButtonClick)
.takeIf { state.steps != state.currentStep },
title = if (state.steps == state.currentStep) {
resourceReference(R.string.common_done)
} else {
state.title
},
containerColor = TangemTheme.colors.background.primary,
modifier = modifier,
)
LinearProgressIndicator(
modifier = Modifier
.padding(horizontal = 16.dp)
.height(4.dp)
.fillMaxWidth(),
progress = { animatedIndicatorFraction },
color = TangemTheme.colors.icon.primary1,
backgroundColor = TangemTheme.colors.background.tertiary,
strokeCap = StrokeCap.Round,
)
}
}
@Preview
@Composable
private fun Preview() {
TangemThemePreview {
Box(
Modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary),
) {
OnboardingStepper(
modifier = Modifier.align(Alignment.TopCenter),
state = OnboardingStepperComponent.StepperState(
currentStep = 2,
steps = 3,
title = resourceReference(R.string.common_done),
),
onBackClick = {},
onSupportButtonClick = {},
)
}
}
}

1
features/onramp/api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,22 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("kotlin-parcelize")
id("configuration")
}
android {
namespace = "com.tangem.features.onramp.api"
}
dependencies {
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
/* Project - Domain */
implementation(projects.domain.wallets.models)
/* Compose */
implementation(deps.compose.runtime)
}

View file

@ -0,0 +1,6 @@
package com.tangem.features.onramp
interface OnrampFeatureToggles {
val isFeatureEnabled: Boolean
}

View file

@ -0,0 +1,22 @@
package com.tangem.features.onramp.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.wallets.models.UserWalletId
/**
* Buy crypto component
*
[REDACTED_AUTHOR]
*/
interface BuyCryptoComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Params, BuyCryptoComponent>
/**
* Params
*
* @property userWalletId user wallet id
*/
data class Params(val userWalletId: UserWalletId)
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.onramp.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface OnrampComponent : ComposableContentComponent {
class Params
interface Factory : ComponentFactory<Params, OnrampComponent>
}

1
features/onramp/impl/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,61 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.features.onramp.impl"
}
dependencies {
/** Project - API */
implementation(projects.features.onramp.api)
/** Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
implementation(projects.core.featuretoggles)
/** Project - Common */
implementation(projects.common.ui)
/** Project - Domain */
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.legacy)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
/** DI */
implementation(deps.hilt.android)
implementation(project(":common:ui"))
kapt(deps.hilt.kapt)
/** AndroidX */
implementation(deps.androidx.activity.compose)
implementation(deps.lifecycle.compose)
/** Compose */
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.foundation)
implementation(deps.compose.material3)
implementation(deps.compose.shimmer)
implementation(deps.compose.coil)
implementation(deps.compose.material)
/** Other */
implementation(deps.decompose.ext.compose)
implementation(deps.kotlin.immutable.collections)
implementation(deps.reKotlin)
implementation(deps.timber)
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.onramp
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
internal class DefaultOnrampFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : OnrampFeatureToggles {
override val isFeatureEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("ONRAMP_ENABLED")
}

View file

@ -0,0 +1,65 @@
package com.tangem.features.onramp.buy
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import arrow.core.getOrElse
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.onramp.component.BuyCryptoComponent
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.selecttoken.OnrampSelectTokenComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.launch
@Stable
internal class DefaultBuyCryptoComponent @AssistedInject constructor(
onrampSelectTokenComponentFactory: OnrampSelectTokenComponent.Factory,
@Assisted private val appComponentContext: AppComponentContext,
@Assisted private val params: BuyCryptoComponent.Params,
private val reduxStateHolder: ReduxStateHolder,
private val getWalletsUseCase: GetWalletsUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
) : BuyCryptoComponent {
private val selectTokenComponent: OnrampSelectTokenComponent = onrampSelectTokenComponentFactory.create(
context = appComponentContext,
params = OnrampSelectTokenComponent.Params(
hasSearchBar = true,
userWalletId = params.userWalletId,
titleResId = R.string.common_buy,
onTokenClick = ::onTokenClick,
),
)
@Composable
override fun Content(modifier: Modifier) {
selectTokenComponent.Content(modifier = modifier)
}
private fun onTokenClick(status: CryptoCurrencyStatus) {
appComponentContext.componentScope.launch {
reduxStateHolder.dispatch(
TradeCryptoAction.Buy(
userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId },
cryptoCurrencyStatus = status,
appCurrencyCode = getSelectedAppCurrencyUseCase.invokeSync()
.getOrElse { AppCurrency.Default }.code,
),
)
}
}
@AssistedFactory
interface Factory : BuyCryptoComponent.Factory {
override fun create(context: AppComponentContext, params: BuyCryptoComponent.Params): DefaultBuyCryptoComponent
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.onramp.buy.di
import com.tangem.features.onramp.buy.DefaultBuyCryptoComponent
import com.tangem.features.onramp.component.BuyCryptoComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface BuyCryptoComponentModule {
@Binds
@Singleton
fun bindBuyCryptoComponentFactory(factory: DefaultBuyCryptoComponent.Factory): BuyCryptoComponent.Factory
}

View file

@ -0,0 +1,16 @@
package com.tangem.features.onramp.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
internal interface ConfirmResidencyComponent : ComposableBottomSheetComponent {
data class Params(
val countryName: String,
val isOnrampSupported: Boolean,
val countryFlagUrl: String,
val onDismiss: () -> Unit,
)
interface Factory : ComponentFactory<Params, ConfirmResidencyComponent>
}

View file

@ -0,0 +1,55 @@
package com.tangem.features.onramp.component.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.features.onramp.component.ConfirmResidencyComponent
import com.tangem.features.onramp.model.ConfirmResidencyModel
import com.tangem.features.onramp.ui.ConfirmResidencyBottomSheet
import com.tangem.features.onramp.ui.ConfirmResidencyBottomSheetContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultConfirmResidencyComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: ConfirmResidencyComponent.Params,
) : ConfirmResidencyComponent, AppComponentContext by context {
private val model: ConfirmResidencyModel = getOrCreateModel(params)
override fun dismiss() {
params.onDismiss()
}
@Composable
override fun BottomSheet() {
val state by model.state.collectAsStateWithLifecycle()
val bottomSheetConfig = remember {
TangemBottomSheetConfig(
isShow = true,
onDismissRequest = ::dismiss,
content = TangemBottomSheetConfigContent.Empty,
)
}
ConfirmResidencyBottomSheet(
config = bottomSheetConfig,
content = { modifier ->
ConfirmResidencyBottomSheetContent(model = state, modifier = modifier)
},
)
}
@AssistedFactory
interface Factory : ConfirmResidencyComponent.Factory {
override fun create(
context: AppComponentContext,
params: ConfirmResidencyComponent.Params,
): DefaultConfirmResidencyComponent
}
}

View file

@ -0,0 +1,64 @@
package com.tangem.features.onramp.component.impl
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.onramp.component.OnrampComponent
import com.tangem.features.onramp.component.ConfirmResidencyComponent
import com.tangem.features.onramp.entity.OnrampBottomSheetConfig
import com.tangem.features.onramp.model.OnrampModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultOnrampComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: OnrampComponent.Params,
private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory,
) : OnrampComponent, AppComponentContext by context {
private val model: OnrampModel = getOrCreateModel(params)
private val confirmResidencyBottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = null,
handleBackButton = false,
childFactory = ::bottomSheetChild,
)
@Composable
override fun Content(modifier: Modifier) {
val bottomSheet by confirmResidencyBottomSheetSlot.subscribeAsState()
BackHandler(onBack = router::pop)
bottomSheet.child?.instance?.BottomSheet()
}
private fun bottomSheetChild(
config: OnrampBottomSheetConfig,
componentContext: ComponentContext,
): ComposableBottomSheetComponent = when (config) {
OnrampBottomSheetConfig.ConfirmResidency -> confirmResidencyComponentFactory.create(
context = childByContext(componentContext),
params = ConfirmResidencyComponent.Params(
countryName = "United States",
isOnrampSupported = true,
countryFlagUrl = "https://hatscripts.github.io/circle-flags/flags/us.svg",
onDismiss = model.bottomSheetNavigation::dismiss,
),
)
}
@AssistedFactory
interface Factory : OnrampComponent.Factory {
override fun create(context: AppComponentContext, params: OnrampComponent.Params): DefaultOnrampComponent
}
}

View file

@ -0,0 +1,46 @@
package com.tangem.features.onramp.component.preview
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.onramp.component.ConfirmResidencyComponent
import com.tangem.features.onramp.entity.ConfirmResidencyUM
import com.tangem.features.onramp.ui.ConfirmResidencyBottomSheet
import com.tangem.features.onramp.ui.ConfirmResidencyBottomSheetContent
import kotlinx.coroutines.flow.MutableStateFlow
internal class PreviewConfirmResidencyComponent(
initialState: ConfirmResidencyUM = ConfirmResidencyUM(
country = "United States",
countryFlagUrl = "https://hatscripts.github.io/circle-flags/flags/us.svg",
isCountrySupported = true,
primaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig(onClick = {}, text = stringReference("Confirm")),
secondaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig(onClick = {}, text = stringReference("Change")),
),
) : ConfirmResidencyComponent {
private val previewState: MutableStateFlow<ConfirmResidencyUM> = MutableStateFlow(initialState)
override fun dismiss() {
/* no-op */
}
@Composable
override fun BottomSheet() {
val state by previewState.collectAsStateWithLifecycle()
val bottomSheetConfig = TangemBottomSheetConfig(
isShow = true,
onDismissRequest = ::dismiss,
content = TangemBottomSheetConfigContent.Empty,
)
ConfirmResidencyBottomSheet(
config = bottomSheetConfig,
content = { modifier ->
ConfirmResidencyBottomSheetContent(model = state, modifier = modifier)
},
)
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.features.onramp.di
import com.tangem.features.onramp.component.ConfirmResidencyComponent
import com.tangem.features.onramp.component.OnrampComponent
import com.tangem.features.onramp.component.impl.DefaultConfirmResidencyComponent
import com.tangem.features.onramp.component.impl.DefaultOnrampComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface OnrampComponentModule {
@Binds
@Singleton
fun bindOnrampComponentFactory(factory: DefaultOnrampComponent.Factory): OnrampComponent.Factory
@Binds
@Singleton
fun bindConfirmResidencyComponentFactory(
factory: DefaultConfirmResidencyComponent.Factory,
): ConfirmResidencyComponent.Factory
}

View file

@ -1,8 +1,8 @@
package com.tangem.features.markets.di
package com.tangem.features.onramp.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.features.markets.DefaultMarketsFeatureToggles
import com.tangem.features.markets.MarketsFeatureToggles
import com.tangem.features.onramp.DefaultOnrampFeatureToggles
import com.tangem.features.onramp.OnrampFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -11,10 +11,11 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object FeatureModule {
internal object OnrampFeatureModule {
@Provides
@Singleton
fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): MarketsFeatureToggles =
DefaultMarketsFeatureToggles(featureTogglesManager = featureTogglesManager)
fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): OnrampFeatureToggles {
return DefaultOnrampFeatureToggles(featureTogglesManager)
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.features.onramp.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.onramp.model.OnrampModel
import com.tangem.features.onramp.model.ConfirmResidencyModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(DecomposeComponent::class)
internal interface OnrampModelModule {
@Binds
@IntoMap
@ClassKey(OnrampModel::class)
fun provideOnrampModel(model: OnrampModel): Model
@Binds
@IntoMap
@ClassKey(ConfirmResidencyModel::class)
fun provideConfirmResidencyModel(model: ConfirmResidencyModel): Model
}

View file

@ -0,0 +1,16 @@
package com.tangem.features.onramp.entity
import com.tangem.core.ui.extensions.TextReference
data class ConfirmResidencyUM(
val country: String,
val countryFlagUrl: String,
val isCountrySupported: Boolean,
val primaryButtonConfig: ActionButtonConfig,
val secondaryButtonConfig: ActionButtonConfig,
) {
data class ActionButtonConfig(
val onClick: () -> Unit,
val text: TextReference,
)
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.onramp.entity
import kotlinx.serialization.Serializable
@Serializable
internal sealed class OnrampBottomSheetConfig {
@Serializable
data object ConfirmResidency : OnrampBottomSheetConfig()
}

View file

@ -0,0 +1,53 @@
package com.tangem.features.onramp.model
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.onramp.component.ConfirmResidencyComponent
import com.tangem.features.onramp.entity.ConfirmResidencyUM
import com.tangem.features.onramp.impl.R
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
@ComponentScoped
internal class ConfirmResidencyModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
) : Model() {
private val params: ConfirmResidencyComponent.Params = paramsContainer.require()
val state: MutableStateFlow<ConfirmResidencyUM> = MutableStateFlow(
ConfirmResidencyUM(
country = params.countryName,
countryFlagUrl = params.countryFlagUrl,
isCountrySupported = params.isOnrampSupported,
primaryButtonConfig = getPrimaryButtonConfig(),
secondaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig(
onClick = ::onChangeClick,
text = resourceReference(R.string.common_change),
),
),
)
private fun getPrimaryButtonConfig() = if (params.isOnrampSupported) {
ConfirmResidencyUM.ActionButtonConfig(
onClick = params.onDismiss,
text = resourceReference(R.string.common_confirm),
)
} else {
ConfirmResidencyUM.ActionButtonConfig(
onClick = ::onCloseClick,
text = resourceReference(R.string.common_close),
)
}
@Suppress("EmptyFunctionBlock")
private fun onCloseClick() {
}
private fun onChangeClick() {
// TODO: [REDACTED_JIRA]
}
}

View file

@ -0,0 +1,38 @@
package com.tangem.features.onramp.model
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.features.onramp.component.OnrampComponent
import com.tangem.features.onramp.entity.OnrampBottomSheetConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("MagicNumber")
@ComponentScoped
internal class OnrampModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
paramsContainer: ParamsContainer,
) : Model() {
@Suppress("UnusedPrivateMember")
private val params: OnrampComponent.Params = paramsContainer.require()
val bottomSheetNavigation: SlotNavigation<OnrampBottomSheetConfig> = SlotNavigation()
init {
modelScope.launch {
delay(1500)
bottomSheetNavigation.activate(OnrampBottomSheetConfig.ConfirmResidency)
}
}
fun pop() {
router.pop()
}
}

View file

@ -0,0 +1,46 @@
package com.tangem.features.onramp.selecttoken
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.features.onramp.selecttoken.ui.OnrampSelectToken
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultOnrampSelectTokenComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
onrampTokenListComponentFactory: OnrampTokenListComponent.Factory,
@Assisted private val params: OnrampSelectTokenComponent.Params,
) : AppComponentContext by appComponentContext, OnrampSelectTokenComponent {
private val onrampTokenListComponent: OnrampTokenListComponent = onrampTokenListComponentFactory.create(
context = child(key = "token_list"),
params = OnrampTokenListComponent.Params(
hasSearchBar = params.hasSearchBar,
userWalletId = params.userWalletId,
onTokenClick = params.onTokenClick,
),
)
@Composable
override fun Content(modifier: Modifier) {
OnrampSelectToken(
titleResId = params.titleResId,
onBackClick = router::pop,
onrampTokenListComponent = onrampTokenListComponent,
modifier = modifier,
)
}
@AssistedFactory
interface Factory : OnrampSelectTokenComponent.Factory {
override fun create(
context: AppComponentContext,
params: OnrampSelectTokenComponent.Params,
): DefaultOnrampSelectTokenComponent
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.features.onramp.selecttoken
import androidx.annotation.StringRes
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWalletId
/**
* Select token component
*
[REDACTED_AUTHOR]
*/
interface OnrampSelectTokenComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Params, OnrampSelectTokenComponent>
/**
* Params
*
* @property hasSearchBar flag that indicates if search bar should be shown
* @property userWalletId id of multi-currency wallet
* @property titleResId resource id of title
* @property onTokenClick callback for token click
*/
data class Params(
val hasSearchBar: Boolean,
val userWalletId: UserWalletId,
@StringRes val titleResId: Int,
val onTokenClick: (CryptoCurrencyStatus) -> Unit,
)
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.onramp.selecttoken.di
import com.tangem.features.onramp.selecttoken.DefaultOnrampSelectTokenComponent
import com.tangem.features.onramp.selecttoken.OnrampSelectTokenComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface OnrampSelectTokenComponentModule {
@Binds
@Singleton
fun bindOnrampSelectTokenComponentFactory(
factory: DefaultOnrampSelectTokenComponent.Factory,
): OnrampSelectTokenComponent.Factory
}

View file

@ -0,0 +1,45 @@
package com.tangem.features.onramp.selecttoken.ui
import androidx.activity.compose.BackHandler
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
@Composable
internal fun OnrampSelectToken(
@StringRes titleResId: Int,
onBackClick: () -> Unit,
onrampTokenListComponent: OnrampTokenListComponent,
modifier: Modifier = Modifier,
) {
BackHandler(onBack = onBackClick)
Column(
modifier = modifier
.background(TangemTheme.colors.background.secondary)
.imePadding()
.systemBarsPadding(),
) {
AppBarWithBackButton(
onBackClick = onBackClick,
text = stringResource(id = titleResId),
iconRes = R.drawable.ic_close_24,
)
onrampTokenListComponent.Content(
contentPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
modifier = Modifier,
)
}
}

View file

@ -0,0 +1,39 @@
package com.tangem.features.onramp.tokenlist
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.onramp.tokenlist.model.OnrampTokenListModel
import com.tangem.features.onramp.tokenlist.ui.TokenList
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Stable
internal class DefaultOnrampTokenListComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: OnrampTokenListComponent.Params,
) : OnrampTokenListComponent, AppComponentContext by context {
private val model: OnrampTokenListModel = getOrCreateModel(params)
@Composable
override fun Content(contentPadding: PaddingValues, modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
TokenList(state = state, contentPadding = contentPadding, modifier = modifier)
}
@AssistedFactory
interface Factory : OnrampTokenListComponent.Factory {
override fun create(
context: AppComponentContext,
params: OnrampTokenListComponent.Params,
): DefaultOnrampTokenListComponent
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.features.onramp.tokenlist
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWalletId
/** Token list component that present list of token for multi-currency wallet */
@Stable
internal interface OnrampTokenListComponent {
@Composable
fun Content(contentPadding: PaddingValues, modifier: Modifier)
/** Component factory */
interface Factory : ComponentFactory<Params, OnrampTokenListComponent>
/**
* Params
*
* @property hasSearchBar flag that indicates if search bar should be shown
* @property userWalletId id of multi-currency wallet
* @property onTokenClick callback for token click
*/
data class Params(
val hasSearchBar: Boolean,
val userWalletId: UserWalletId,
val onTokenClick: (CryptoCurrencyStatus) -> Unit,
)
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.onramp.tokenlist.di
import com.tangem.features.onramp.tokenlist.DefaultOnrampTokenListComponent
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface OnrampTokenListComponentModule {
@Binds
@Singleton
fun bindOnrampTokenListComponentFactory(
factory: DefaultOnrampTokenListComponent.Factory,
): OnrampTokenListComponent.Factory
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.onramp.tokenlist.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.onramp.tokenlist.model.OnrampTokenListModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(DecomposeComponent::class)
internal interface OnrampTokenListModelModule {
@Binds
@IntoMap
@ClassKey(OnrampTokenListModel::class)
fun bindOnrampTokenListModel(model: OnrampTokenListModel): Model
}

View file

@ -0,0 +1,35 @@
package com.tangem.features.onramp.tokenlist.entity
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* Token list UM
*
* @property items items (search bar, tokens, headers)
* @property isBalanceHidden flag that indicates if balance should be hidden
*
[REDACTED_AUTHOR]
*/
internal data class TokenListUM(
val items: ImmutableList<TokensListItemUM>,
val isBalanceHidden: Boolean,
) {
/** Get search bar if it exists */
fun getSearchBar(): TokensListItemUM.SearchBar? {
return items.firstOrNull() as? TokensListItemUM.SearchBar
}
/** Get tokens */
fun getTokens(): ImmutableList<TokensListItemUM> {
if (getSearchBar() == null) return items
return if (items.size > 1) {
items.subList(fromIndex = 1, toIndex = items.size)
} else {
persistentListOf()
}
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.features.onramp.tokenlist.entity
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import timber.log.Timber
import javax.inject.Inject
/**
* [TokenListUM] controller
*
[REDACTED_AUTHOR]
*/
internal class TokenListUMController @Inject constructor() {
val state: StateFlow<TokenListUM> get() = _state
private val _state: MutableStateFlow<TokenListUM> = MutableStateFlow(
value = TokenListUM(
items = persistentListOf(),
isBalanceHidden = false,
),
)
fun update(transform: (TokenListUM) -> TokenListUM) {
Timber.d("Applying non-name transformation")
_state.update(transform)
}
fun update(transformer: TokenListUMTransformer) {
Timber.d("Applying ${transformer::class.simpleName}")
_state.update(transformer::transform)
}
/** Get search bar if it exists */
fun getSearchBar(): TokensListItemUM.SearchBar? {
return _state.value.getSearchBar()
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.onramp.tokenlist.entity
import com.tangem.utils.transformer.Transformer
/**
* Base [TokenListUM] transformer
*
[REDACTED_AUTHOR]
*/
internal interface TokenListUMTransformer : Transformer<TokenListUM>

View file

@ -0,0 +1,33 @@
package com.tangem.features.onramp.tokenlist.entity.transformer
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import kotlinx.collections.immutable.persistentListOf
/**
* Base [SearchBarUM] transformer
*
[REDACTED_AUTHOR]
*/
internal abstract class SearchBarUMTransformer : TokenListUMTransformer {
abstract fun transform(prevState: SearchBarUM): SearchBarUM
override fun transform(prevState: TokenListUM): TokenListUM {
val searchBarItem = prevState.getSearchBar()
return if (searchBarItem != null) {
val updatedSearchBar = searchBarItem.copy(searchBarUM = transform(searchBarItem.searchBarUM))
prevState.copy(
items = persistentListOf(
updatedSearchBar,
*prevState.getTokens().toTypedArray(),
),
)
} else {
prevState
}
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.features.onramp.tokenlist.entity.transformer
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.onramp.impl.R
internal class UpdateSearchBarActiveStateTransformer(private val isActive: Boolean) : SearchBarUMTransformer() {
override fun transform(prevState: SearchBarUM): SearchBarUM {
val placeholderText = if (isActive) {
TextReference.EMPTY
} else {
resourceReference(id = R.string.common_search)
}
return prevState.copy(placeholderText = placeholderText, isActive = isActive)
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.onramp.tokenlist.entity.transformer
import com.tangem.core.ui.components.fields.entity.SearchBarUM
internal class UpdateSearchQueryTransformer(private val newQuery: String) : SearchBarUMTransformer() {
override fun transform(prevState: SearchBarUM): SearchBarUM {
return prevState.copy(query = newQuery)
}
}

View file

@ -0,0 +1,48 @@
package com.tangem.features.onramp.tokenlist.entity.transformer
import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import kotlinx.collections.immutable.toImmutableList
internal class UpdateTokenItemsTransformer(
private val tokenItemStateConverter: TokenItemStateConverter,
private val statuses: List<CryptoCurrencyStatus>,
private val isBalanceHidden: Boolean,
private val hasSearchBar: Boolean,
private val onQueryChange: (String) -> Unit,
private val onActiveChange: (Boolean) -> Unit,
) : TokenListUMTransformer {
override fun transform(prevState: TokenListUM): TokenListUM {
val items = tokenItemStateConverter.convertList(input = statuses).map(TokensListItemUM::Token)
val searchBarItem = if (hasSearchBar) {
prevState.getSearchBar() ?: createSearchBarItem()
} else {
null
}
return prevState.copy(
items = (listOfNotNull(searchBarItem) + items).toImmutableList(),
isBalanceHidden = isBalanceHidden,
)
}
private fun createSearchBarItem(): TokensListItemUM.SearchBar {
return TokensListItemUM.SearchBar(
searchBarUM = SearchBarUM(
placeholderText = resourceReference(id = R.string.common_search),
query = "",
onQueryChange = onQueryChange,
isActive = false,
onActiveChange = onActiveChange,
),
)
}
}

View file

@ -0,0 +1,105 @@
package com.tangem.features.onramp.tokenlist.model
import arrow.core.getOrElse
import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenList
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMController
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateSearchQueryTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer
import com.tangem.features.onramp.tokenlist.utils.SearchTokensManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
internal class OnrampTokenListModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val tokenListUMController: TokenListUMController,
private val searchTokensManager: SearchTokensManager,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getTokenListUseCase: GetTokenListUseCase,
) : Model() {
val state: StateFlow<TokenListUM> = tokenListUMController.state
private val params: OnrampTokenListComponent.Params = paramsContainer.require()
init {
subscribeOnUpdateState()
}
private fun subscribeOnUpdateState() {
combine(
flow = getTokenListUseCase.launch(userWalletId = params.userWalletId).distinctUntilChanged(),
flow2 = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(),
flow3 = getBalanceHidingSettingsUseCase().map { it.isBalanceHidden }.distinctUntilChanged(),
flow4 = searchTokensManager.query,
) { maybeTokenList, appCurrency, isBalanceHidden, query ->
val currencies = maybeTokenList.getOrElse(
ifLoading = { it ?: TokenList.Empty },
ifError = { TokenList.Empty },
)
.flattenCurrencies()
val filterTokenList = currencies
.filterByQuery(query = query)
.filterByAvailability()
UpdateTokenItemsTransformer(
tokenItemStateConverter = TokenItemStateConverter(
appCurrency = appCurrency,
onItemClick = params.onTokenClick,
),
statuses = filterTokenList,
isBalanceHidden = isBalanceHidden,
hasSearchBar = params.hasSearchBar && currencies.isNotEmpty(),
onQueryChange = ::onSearchQueryChange,
onActiveChange = ::onSearchBarActiveChange,
)
}
.onEach(tokenListUMController::update)
.flowOn(dispatchers.main)
.launchIn(modelScope)
}
private fun onSearchQueryChange(newQuery: String) {
val searchBar = tokenListUMController.getSearchBar()
if (searchBar?.searchBarUM?.query == newQuery) return
modelScope.launch {
tokenListUMController.update(transformer = UpdateSearchQueryTransformer(newQuery))
searchTokensManager.update(newQuery)
}
}
private fun onSearchBarActiveChange(isActive: Boolean) {
tokenListUMController.update(transformer = UpdateSearchBarActiveStateTransformer(isActive))
}
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)
}
}
// TODO: [REDACTED_JIRA]
private fun List<CryptoCurrencyStatus>.filterByAvailability(): List<CryptoCurrencyStatus> {
return this
}
}

View file

@ -0,0 +1,65 @@
package com.tangem.features.onramp.tokenlist.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.tokenlist.TokenListItem
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.ui.preview.PreviewTokenListUMProvider
/**
* Token list
*
* @param state state
* @param modifier modifier
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun TokenList(state: TokenListUM, contentPadding: PaddingValues, modifier: Modifier = Modifier) {
LazyColumn(modifier = modifier, contentPadding = contentPadding) {
itemsIndexed(
items = state.items,
key = { _, item -> item.id },
contentType = { _, item -> item::class.java },
itemContent = { index, item ->
TokenListItem(
state = item,
isBalanceHidden = state.isBalanceHidden,
modifier = Modifier
.animateItem()
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = state.items.lastIndex,
addDefaultPadding = false,
backgroundColor = TangemTheme.colors.background.primary,
),
)
},
)
}
}
@Preview
@Composable
private fun Preview_TokenList(@PreviewParameter(PreviewTokenListUMProvider::class) state: TokenListUM) {
TangemThemePreview {
TokenList(
state = state,
contentPadding = PaddingValues(all = 16.dp),
modifier = Modifier
.fillMaxWidth()
.background(color = TangemTheme.colors.background.secondary),
)
}
}

View file

@ -0,0 +1,62 @@
package com.tangem.features.onramp.tokenlist.ui.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import kotlinx.collections.immutable.toImmutableList
internal class PreviewTokenListUMProvider : PreviewParameterProvider<TokenListUM> {
override val values: Sequence<TokenListUM> = sequenceOf(
createTokensList(hasSearchBar = false, createDefaultTokenItem()),
createTokensList(hasSearchBar = true, createDefaultTokenItem()),
)
private fun createTokensList(hasSearchBar: Boolean, vararg items: TokensListItemUM): TokenListUM {
return TokenListUM(
items = buildList {
if (hasSearchBar) {
TokensListItemUM.SearchBar(
searchBarUM = SearchBarUM(
placeholderText = resourceReference(id = R.string.common_search),
query = "",
onQueryChange = {},
isActive = false,
onActiveChange = {},
),
)
.let { add(element = it) }
}
addAll(items)
}
.toImmutableList(),
isBalanceHidden = false,
)
}
private fun createDefaultTokenItem(): TokensListItemUM.Token {
return TokensListItemUM.Token(
state = TokenItemState.Content(
id = "1",
iconState = CurrencyIconState.Locked,
titleState = TokenItemState.TitleState.Content(text = "Bitcoin"),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "0,35853044 BTC"),
subtitleState = TokenItemState.SubtitleState.CryptoPriceContent(
price = "34 496,75 \$",
priceChangePercent = "0,43 %",
type = PriceChangeType.DOWN,
),
onItemClick = {},
onItemLongClick = {},
),
)
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.features.onramp.tokenlist.utils
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.withDebounce
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
/**
* Search tokens manager
*
[REDACTED_AUTHOR]
*/
internal class SearchTokensManager @Inject constructor() {
val query: Flow<String>
get() = _query
private val _query = MutableStateFlow(value = "")
private val jobHolder = JobHolder()
suspend fun update(value: String) {
coroutineScope {
if (value.isEmpty()) {
_query.value = value
} else {
withDebounce(jobHolder) { _query.value = value }
}
}
}
}

View file

@ -0,0 +1,165 @@
package com.tangem.features.onramp.ui
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onramp.component.ConfirmResidencyComponent
import com.tangem.features.onramp.component.preview.PreviewConfirmResidencyComponent
import com.tangem.features.onramp.entity.ConfirmResidencyUM
import com.tangem.features.onramp.impl.R
@Composable
internal fun ConfirmResidencyBottomSheet(config: TangemBottomSheetConfig, content: @Composable (Modifier) -> Unit) {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = config,
addBottomInsets = true,
containerColor = TangemTheme.colors.background.tertiary,
title = { _ -> Title() },
content = {
val contentModifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(bottom = TangemTheme.dimens.spacing16)
.fillMaxWidth()
.wrapContentHeight()
content(contentModifier)
},
)
}
@Composable
private fun Title(modifier: Modifier = Modifier) {
TangemTopAppBar(
modifier = modifier,
title = resourceReference(R.string.onramp_residency_bottomsheet_title),
titleAlignment = Alignment.CenterHorizontally,
height = TangemTopAppBarHeight.BOTTOM_SHEET,
)
}
@Composable
internal fun ConfirmResidencyBottomSheetContent(model: ConfirmResidencyUM, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
CountryContent(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens.spacing24),
name = model.country,
flagUrl = model.countryFlagUrl,
isCountrySupported = model.isCountrySupported,
)
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = model.secondaryButtonConfig.text.resolveReference(),
onClick = model.secondaryButtonConfig.onClick,
)
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing12),
text = model.primaryButtonConfig.text.resolveReference(),
onClick = model.primaryButtonConfig.onClick,
)
}
}
@Composable
private fun CountryContent(name: String, flagUrl: String, isCountrySupported: Boolean, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
val flagSize = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() }
SubcomposeAsyncImage(
modifier = Modifier.size(TangemTheme.dimens.size36),
model = ImageRequest.Builder(LocalContext.current)
.size(size = flagSize)
.data(flagUrl)
.memoryCacheKey(flagUrl + flagSize)
.crossfade(true)
.allowHardware(false)
.build(),
loading = { CircleShimmer() },
contentDescription = null,
)
Text(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
text = name,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle1,
maxLines = 1,
)
Text(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing6),
text = stringResource(
id = if (isCountrySupported) {
R.string.onramp_residency_bottomsheet_country_subtitle
} else {
R.string.onramp_residency_bottomsheet_country_not_supported
},
),
color = if (isCountrySupported) TangemTheme.colors.text.tertiary else TangemTheme.colors.text.warning,
style = TangemTheme.typography.body2,
)
}
}
// region Preview
@Composable
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_ConfirmResidencyBottomSheet(
@PreviewParameter(ConfirmResidencyComponentPreviewProvider::class) component: ConfirmResidencyComponent,
) {
TangemThemePreview {
component.BottomSheet()
}
}
private class ConfirmResidencyComponentPreviewProvider : PreviewParameterProvider<ConfirmResidencyComponent> {
override val values: Sequence<ConfirmResidencyComponent>
get() = sequenceOf(
PreviewConfirmResidencyComponent(),
PreviewConfirmResidencyComponent(
initialState = ConfirmResidencyUM(
country = "Russia",
countryFlagUrl = "https://hatscripts.github.io/circle-flags/flags/ru.svg",
isCountrySupported = false,
primaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig(
onClick = {},
text = stringReference("Close"),
),
secondaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig(
onClick = {},
text = stringReference("Change"),
),
),
),
)
}

View file

@ -4,6 +4,8 @@ import com.tangem.common.extensions.isZero
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
import com.tangem.core.ui.utils.toTimeFormat
@ -17,7 +19,6 @@ import com.tangem.features.send.impl.presentation.state.recipient.utils.RECENT_K
import com.tangem.features.send.impl.presentation.state.recipient.utils.emptyListState
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.toFormattedCurrencyString
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
@ -76,10 +77,7 @@ internal class SendRecipientHistoryListConverter(
}
private fun TxHistoryItem.getAmount(cryptoCurrency: CryptoCurrency): String {
return amount.toFormattedCurrencyString(
currency = cryptoCurrency.symbol,
decimals = cryptoCurrency.decimals,
)
return amount.format { crypto(cryptoCurrency) }
}
private fun TxHistoryItem.extractTimestamp(): TextReference {

View file

@ -32,6 +32,9 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fee
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.features.send.impl.presentation.state.SendStates
@ -228,12 +231,14 @@ private fun SendStates.FeeState.getFiatValue() = if (isFeeConvertibleToFiat) {
)
} else {
val amount = fee?.amount
BigDecimalFormatter.formatCryptoFeeAmount(
cryptoAmount = amount?.value,
cryptoCurrency = amount?.currencySymbol.orEmpty(),
decimals = amount?.decimals ?: 0,
canBeLower = isFeeApproximate,
)
amount?.value.format {
crypto(
decimals = amount?.decimals ?: 0,
symbol = amount?.currencySymbol.orEmpty(),
).fee(
canBeLower = isFeeApproximate,
)
}
}
private fun getButtonData(

View file

@ -16,6 +16,9 @@ import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.rows.SelectorRowItem
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fee
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.parseToBigDecimal
@ -53,12 +56,12 @@ internal fun SendSpeedSelectorItem(
onSelect = onSelect,
modifier = modifier,
preDot = stringReference(
BigDecimalFormatter.formatCryptoFeeAmount(
cryptoAmount = amount?.value,
cryptoCurrency = amount?.currencySymbol.orEmpty(),
decimals = amount?.decimals ?: 0,
canBeLower = state.isFeeApproximate,
),
amount?.value.format {
crypto(
symbol = amount?.currencySymbol.orEmpty(),
decimals = amount?.decimals ?: 0,
).fee(canBeLower = state.isFeeApproximate)
},
),
postDot = if (state.isFeeConvertibleToFiat) {
getFiatReference(amount?.value, state.rate, state.appCurrency)

View file

@ -18,6 +18,9 @@ import com.tangem.common.ui.amountScreen.utils.getFiatReference
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.rows.SelectorRowItem
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fee
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.BigDecimalFormatter
@ -62,12 +65,12 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, o
titleRes = title,
iconRes = icon,
preDot = stringReference(
BigDecimalFormatter.formatCryptoFeeAmount(
cryptoAmount = feeAmount?.value,
cryptoCurrency = feeAmount?.currencySymbol.orEmpty(),
decimals = feeAmount?.decimals ?: 0,
canBeLower = feeState.isFeeApproximate,
),
feeAmount?.value.format {
crypto(
symbol = feeAmount?.currencySymbol.orEmpty(),
decimals = feeAmount?.decimals ?: 0,
).fee(canBeLower = feeState.isFeeApproximate)
},
),
postDot = if (feeState.isFeeConvertibleToFiat) {
getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency)

View file

@ -32,10 +32,10 @@ internal class StakingAnalyticSender(
fun confirmationScreen(value: StakingUiState) {
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
val validatorState = confirmationState?.validatorState as? ValidatorState.Content
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
val validatorName = validatorState?.chosenValidator?.name ?: return
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) return
if (confirmationState?.innerState == InnerConfirmationStakingState.COMPLETED) return
analyticsEventHandler.send(
StakingAnalyticsEvent.ConfirmationScreenOpened(
@ -53,6 +53,7 @@ internal class StakingAnalyticSender(
StakingStep.Amount -> StakeScreenSource.Amount
StakingStep.Confirmation -> StakeScreenSource.Confirmation
StakingStep.Validators,
StakingStep.RestakeValidator,
StakingStep.RewardsValidators,
-> StakeScreenSource.Validators
},
@ -75,8 +76,7 @@ internal class StakingAnalyticSender(
}
fun sendTransactionStakingAnalytics(value: StakingUiState) {
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
val validatorState = confirmationState?.validatorState as? ValidatorState.Content
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
val validatorName = validatorState?.chosenValidator?.name ?: return
analyticsEventHandler.send(
@ -98,8 +98,7 @@ internal class StakingAnalyticSender(
}
fun sendTransactionStakingClickedAnalytics(value: StakingUiState) {
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
val validatorState = confirmationState?.validatorState as? ValidatorState.Content
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
val validatorName = validatorState?.chosenValidator?.name ?: return
analyticsEventHandler.send(
@ -114,11 +113,9 @@ internal class StakingAnalyticSender(
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
return when (value.actionType) {
StakingActionCommonType.ENTER -> StakingActionType.STAKE
StakingActionCommonType.EXIT -> StakingActionType.UNSTAKE
StakingActionCommonType.PENDING_REWARDS,
StakingActionCommonType.PENDING_OTHER,
-> confirmationState?.pendingAction?.type ?: StakingActionType.UNKNOWN
StakingActionCommonType.Enter -> StakingActionType.STAKE
StakingActionCommonType.Exit -> StakingActionType.UNSTAKE
is StakingActionCommonType.Pending -> confirmationState?.pendingAction?.type ?: StakingActionType.UNKNOWN
}
}
}

View file

@ -45,6 +45,12 @@ internal class StakingStateController @Inject constructor(
mutableUiState.update(function = titleTransformer::transform)
}
fun updateAll(vararg transformer: Transformer<StakingUiState>) {
transformer.forEach { mutableUiState.update(function = it::transform) }
mutableUiState.update(function = buttonsTransformer::transform)
mutableUiState.update(function = titleTransformer::transform)
}
fun clear() {
mutableUiState.update { getInitialState() }
mutableUiState.update(function = buttonsTransformer::transform)
@ -72,12 +78,13 @@ internal class StakingStateController @Inject constructor(
currentStep = StakingStep.InitialInfo,
initialInfoState = StakingStates.InitialInfoState.Empty(),
amountState = AmountState.Empty(),
validatorState = StakingStates.ValidatorState.Empty(),
rewardsValidatorsState = StakingStates.RewardsValidatorsState.Empty(),
confirmationState = StakingStates.ConfirmationState.Empty(),
isBalanceHidden = false,
event = consumedEvent(),
bottomSheetConfig = null,
actionType = StakingActionCommonType.ENTER,
actionType = StakingActionCommonType.Enter,
buttonsState = NavigationButtonsState.Empty,
)
}

View file

@ -23,12 +23,14 @@ internal class StakingStateRouter(
fun onNextClick() {
when (stateController.value.currentStep) {
StakingStep.InitialInfo -> when (stateController.value.actionType) {
StakingActionCommonType.ENTER -> showAmount()
StakingActionCommonType.PENDING_OTHER,
StakingActionCommonType.EXIT,
StakingActionCommonType.Enter -> showAmount()
StakingActionCommonType.Pending.Other,
StakingActionCommonType.Exit,
StakingActionCommonType.Pending.Rewards,
-> showConfirmation()
StakingActionCommonType.PENDING_REWARDS -> showRewardsValidators()
StakingActionCommonType.Pending.Restake -> showRestakeValidators()
}
StakingStep.RestakeValidator,
StakingStep.RewardsValidators,
StakingStep.Validators,
StakingStep.Amount,
@ -41,25 +43,31 @@ internal class StakingStateRouter(
val uiState = stateController.uiState.value
when (uiState.currentStep) {
StakingStep.InitialInfo -> onBackClick()
StakingStep.Amount -> showInitial()
StakingStep.RestakeValidator,
StakingStep.RewardsValidators,
StakingStep.Amount,
-> showInitial()
StakingStep.Confirmation -> {
if (uiState.actionType != StakingActionCommonType.ENTER) {
if (uiState.actionType != StakingActionCommonType.Enter) {
showInitial()
} else {
showAmount()
}
}
StakingStep.Validators -> showConfirmation()
StakingStep.RewardsValidators -> showInitial()
}
}
fun showValidators() {
stateController.update { it.copy(currentStep = StakingStep.Validators) }
}
private fun showInitial() {
analyticSender.initialInfoScreen(stateController.value)
stateController.update { it.copy(currentStep = StakingStep.InitialInfo) }
}
private fun showRewardsValidators() {
fun showRewardsValidators() {
analyticsEventsHandler.send(StakingAnalyticsEvent.RewardScreenOpened)
stateController.update { it.copy(currentStep = StakingStep.RewardsValidators) }
}
@ -69,8 +77,8 @@ internal class StakingStateRouter(
stateController.update { it.copy(currentStep = StakingStep.Amount) }
}
fun showValidators() {
stateController.update { it.copy(currentStep = StakingStep.Validators) }
private fun showRestakeValidators() {
stateController.update { it.copy(currentStep = StakingStep.RestakeValidator) }
}
private fun showConfirmation() {

View file

@ -9,8 +9,8 @@ import com.tangem.core.ui.components.list.RoundedListWithDividersItemData
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig
import com.tangem.domain.staking.model.PendingTransaction
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
import com.tangem.features.staking.impl.presentation.state.events.StakingEvent
@ -34,6 +34,7 @@ internal data class StakingUiState(
val amountState: AmountState,
val rewardsValidatorsState: StakingStates.RewardsValidatorsState,
val confirmationState: StakingStates.ConfirmationState,
val validatorState: StakingStates.ValidatorState,
val isBalanceHidden: Boolean,
val bottomSheetConfig: TangemBottomSheetConfig?,
val actionType: StakingActionCommonType,
@ -45,10 +46,12 @@ internal data class StakingUiState(
initialInfoState: StakingStates.InitialInfoState = this.initialInfoState,
amountState: AmountState = this.amountState,
confirmationState: StakingStates.ConfirmationState = this.confirmationState,
validatorState: StakingStates.ValidatorState = this.validatorState,
): StakingUiState = copy(
initialInfoState = initialInfoState,
amountState = amountState,
confirmationState = confirmationState,
validatorState = validatorState,
)
}
@ -85,13 +88,30 @@ internal sealed class StakingStates {
) : RewardsValidatorsState()
}
sealed class ValidatorState : StakingStates() {
abstract val isClickable: Boolean
data class Data(
override val isPrimaryButtonEnabled: Boolean,
override val isClickable: Boolean,
val isVisibleOnConfirmation: Boolean,
val chosenValidator: Yield.Validator,
val activeValidator: Yield.Validator?,
val availableValidators: List<Yield.Validator>,
) : ValidatorState()
data class Empty(
override val isClickable: Boolean = false,
override val isPrimaryButtonEnabled: Boolean = false,
) : ValidatorState()
}
/** Confirmation state */
sealed class ConfirmationState : StakingStates() {
data class Data(
override val isPrimaryButtonEnabled: Boolean,
val innerState: InnerConfirmationStakingState,
val feeState: FeeState,
val validatorState: ValidatorState,
val pendingAction: PendingAction?,
val pendingActions: ImmutableList<PendingAction>?,
val notifications: ImmutableList<NotificationUM>,
@ -99,7 +119,6 @@ internal sealed class StakingStates {
val transactionDoneState: TransactionDoneState,
val isApprovalNeeded: Boolean,
val reduceAmountBy: BigDecimal?,
val possiblePendingTransaction: PendingTransaction?,
) : ConfirmationState()
data class Empty(
@ -112,6 +131,7 @@ enum class StakingStep {
InitialInfo,
RewardsValidators,
Amount,
RestakeValidator,
Confirmation,
Validators,
}

View file

@ -1,34 +0,0 @@
package com.tangem.features.staking.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.domain.staking.model.stakekit.Yield
@Immutable
internal sealed class ValidatorState {
abstract val isClickable: Boolean
data class Content(
override val isClickable: Boolean,
val chosenValidator: Yield.Validator,
val availableValidators: List<Yield.Validator>,
) : ValidatorState()
data object Loading : ValidatorState() {
override val isClickable: Boolean
get() = false
}
data object Error : ValidatorState() {
override val isClickable: Boolean
get() = false
}
fun copySealed(isClickable: Boolean): ValidatorState {
return if (this is Content) {
copy(isClickable = isClickable)
} else {
this
}
}
}

View file

@ -1,6 +1,8 @@
package com.tangem.features.staking.impl.presentation.state.converters
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
@ -47,10 +49,7 @@ internal class BalanceItemConverter(
cryptoValue = cryptoAmount.parseBigDecimal(cryptoCurrency.decimals),
cryptoDecimal = cryptoAmount,
cryptoAmount = stringReference(
BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = cryptoAmount,
cryptoCurrency = cryptoCurrency,
),
cryptoAmount.format { crypto(cryptoCurrency) },
),
fiatAmount = stringReference(
BigDecimalFormatter.formatFiatAmount(

View file

@ -1,6 +1,8 @@
package com.tangem.features.staking.impl.presentation.state.converters
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
@ -65,10 +67,9 @@ internal class RewardsValidatorStateConverter(
val appCurrency = appCurrencyProvider()
val cryptoCurrency = cryptoCurrencyStatus.currency
val cryptoAmount = stringReference(
BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = cryptoValue,
cryptoCurrency = cryptoCurrency,
),
cryptoValue.format {
crypto(cryptoCurrency)
},
)
val fiatAmount = stringReference(
BigDecimalFormatter.formatFiatAmount(

View file

@ -1,6 +1,8 @@
package com.tangem.features.staking.impl.presentation.state.converters
import com.tangem.common.extensions.isZero
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.*
@ -41,10 +43,7 @@ internal class YieldBalancesConverter(
}
val (type, isActionable) = getRewardBlockType()
InnerYieldBalanceState.Data(
rewardsCrypto = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = cryptoRewardsValue,
cryptoCurrency = cryptoCurrency,
),
rewardsCrypto = cryptoRewardsValue.format { crypto(cryptoCurrency) },
rewardsFiat = BigDecimalFormatter.formatFiatAmount(
fiatAmount = fiatRewardsValue,
fiatCurrencyCode = appCurrency.code,

View file

@ -1,6 +1,9 @@
package com.tangem.features.staking.impl.presentation.state.helpers
import com.tangem.domain.staking.FetchStakingYieldBalanceUseCase
import com.tangem.domain.staking.FetchActionsUseCase
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -20,16 +23,21 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase,
private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase,
private val fetchActionsUseCase: FetchActionsUseCase,
@DelayedWork private val coroutineScope: CoroutineScope,
@Assisted private val userWallet: UserWallet,
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@Assisted private val yield: Yield,
) {
fun scheduleUpdates() {
fun fullUpdate() {
coroutineScope.launch {
listOf(
// we should update network to find pending tx after 1 sec
async {
fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrencyStatus.currency.network))
fetchPendingTransactionsUseCase(
userWalletId = userWallet.walletId,
networks = setOf(cryptoCurrencyStatus.currency.network),
)
},
// we should update tx history and network for new balances
async {
@ -41,11 +49,14 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
async {
updateNetworkStatuses()
},
async {
updateProcessingActions()
},
).awaitAll()
}
}
suspend fun instantUpdate() {
suspend fun partialUpdate() {
coroutineScope {
listOf(
async {
@ -54,6 +65,19 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
async {
updateNetworkStatuses(delay = 0)
},
async {
updateProcessingActions()
},
).awaitAll()
}
}
suspend fun initialUpdate() {
coroutineScope {
listOf(
async {
updateStakeBalance()
},
).awaitAll()
}
}
@ -91,9 +115,22 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
}
}
private suspend fun updateProcessingActions() {
fetchActionsUseCase(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
networkType = yield.token.network,
stakingActionStatus = StakingActionStatus.PROCESSING,
)
}
@AssistedFactory
interface Factory {
fun create(cryptoCurrencyStatus: CryptoCurrencyStatus, userWallet: UserWallet): StakingBalanceUpdater
fun create(
cryptoCurrencyStatus: CryptoCurrencyStatus,
userWallet: UserWallet,
yield: Yield,
): StakingBalanceUpdater
}
private companion object {

View file

@ -22,8 +22,7 @@ import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.staking.impl.presentation.state.StakingStateController
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.ValidatorState
import com.tangem.features.staking.impl.presentation.state.utils.isSolanaWithdraw
import com.tangem.features.staking.impl.presentation.state.utils.isComposePendingActions
import com.tangem.utils.extensions.orZero
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -48,8 +47,6 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
) {
suspend fun getFee(
pendingAction: PendingAction?,
pendingActions: ImmutableList<PendingAction>?,
onStakingFee: (Fee) -> Unit,
onStakingFeeError: (StakingError) -> Unit,
onApprovalFee: (TransactionFee) -> Unit,
@ -57,17 +54,20 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
) {
val state = stateController.value
val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data
?: error("No confirmation state")
val validatorState = confirmationState.validatorState as? ValidatorState.Content
?: error("Illegal state")
val validatorState = state.validatorState as? StakingStates.ValidatorState.Data
?: error("No validator provided")
val amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided")
val pendingAction = confirmationState.pendingAction
val pendingActions = confirmationState.pendingActions
val validatorAddress = validatorState.chosenValidator.address
val approval = stakingApproval as? StakingApproval.Needed
if (approval != null && state.actionType == StakingActionCommonType.ENTER) {
if (approval != null && state.actionType == StakingActionCommonType.Enter) {
val allowance = getAllowanceUseCase(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
@ -114,7 +114,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
val sourceAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
?: error("No available address")
val gasEstimate = if (isSolanaWithdraw(cryptoCurrencyStatus.currency.network.id.value, pendingActions)) {
val gasEstimate = if (isComposePendingActions(cryptoCurrencyStatus.currency.network.id.value, pendingActions)) {
val result = coroutineScope {
pendingActions?.map { action ->
async {

View file

@ -5,7 +5,6 @@ import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.domain.staking.*
import com.tangem.domain.staking.model.PendingTransaction
import com.tangem.domain.staking.model.SubmitHashData
import com.tangem.domain.staking.model.stakekit.*
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
@ -21,7 +20,7 @@ import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount
import com.tangem.features.staking.impl.presentation.state.utils.isSolanaWithdraw
import com.tangem.features.staking.impl.presentation.state.utils.isComposePendingActions
import com.tangem.utils.extensions.orZero
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -31,7 +30,6 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import timber.log.Timber
import java.math.BigDecimal
import java.util.UUID
@Suppress("LongParameterList")
internal class StakingTransactionSender @AssistedInject constructor(
@ -43,7 +41,6 @@ internal class StakingTransactionSender @AssistedInject constructor(
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val submitHashUseCase: SubmitHashUseCase,
private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase,
private val savePendingTransactionUseCase: SavePendingTransactionUseCase,
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@Assisted private val userWallet: UserWallet,
@Assisted private val yield: Yield,
@ -51,7 +48,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
) {
private val balanceUpdater: StakingBalanceUpdater
get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet)
get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet, yield)
suspend fun constructAndSendTransactions(
onConstructSuccess: (List<StakingTransaction>) -> Unit,
@ -89,26 +86,21 @@ internal class StakingTransactionSender @AssistedInject constructor(
sendStakingTransaction(
fullTransactionsData = fullTransactionsData,
possiblePendingTransaction = composePendingTransaction(confirmationState),
onSendSuccess = onSendSuccess,
onSendError = onSendError,
)
}
private fun composePendingTransaction(confirmationState: StakingStates.ConfirmationState.Data): PendingTransaction {
return confirmationState.possiblePendingTransaction ?: composeStakeTransaction(confirmationState)
}
private suspend fun getStakingTransactions(
state: StakingUiState,
confirmationState: StakingStates.ConfirmationState.Data,
onConstructError: (StakingError) -> Unit,
) = coroutineScope {
val isAllWithdrawAction = isSolanaWithdraw(
val isComposePendingActions = isComposePendingActions(
cryptoCurrencyStatus.currency.network.id.value,
confirmationState.pendingActions,
)
if (isAllWithdrawAction) {
if (isComposePendingActions) {
confirmationState.pendingActions?.map { action ->
async {
getStakingTransaction(
@ -169,7 +161,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
action: PendingAction? = confirmationState.pendingAction,
onConstructError: (StakingError) -> Unit,
): List<StakingTransaction> {
val validatorState = confirmationState.validatorState as? ValidatorState.Content
val validatorState = state.validatorState as? StakingStates.ValidatorState.Data
?: error("No validator provided")
val fee = (confirmationState.feeState as? FeeState.Content)?.fee
?: error("No fee provided")
@ -202,7 +194,6 @@ internal class StakingTransactionSender @AssistedInject constructor(
private suspend fun sendStakingTransaction(
fullTransactionsData: List<FullTransactionData>,
possiblePendingTransaction: PendingTransaction,
onSendSuccess: (txUrl: String) -> Unit,
onSendError: (SendTransactionError?) -> Unit,
) {
@ -218,24 +209,19 @@ internal class StakingTransactionSender @AssistedInject constructor(
submitHash(
transactions = fullTransactionsData.map { it.stakeKitTransaction },
transactionHashes = transactionHashes,
pendingTransaction = possiblePendingTransaction,
)
val txUrl = getExplorerTransactionUrlUseCase(
txHash = transactionHashes.last(),
networkId = cryptoCurrencyStatus.currency.network.id,
).getOrElse { "" }
balanceUpdater.scheduleUpdates()
balanceUpdater.fullUpdate()
onSendSuccess(txUrl)
},
)
}
private suspend fun submitHash(
transactions: List<StakingTransaction>,
transactionHashes: List<String>,
pendingTransaction: PendingTransaction,
) {
private suspend fun submitHash(transactions: List<StakingTransaction>, transactionHashes: List<String>) {
transactions
.zip(transactionHashes)
.forEach { (transaction, transactionHash) ->
@ -243,7 +229,6 @@ internal class StakingTransactionSender @AssistedInject constructor(
SubmitHashData(
transactionId = transaction.id,
transactionHash = transactionHash,
pendingTransaction = pendingTransaction,
),
)
.onLeft {
@ -253,9 +238,6 @@ internal class StakingTransactionSender @AssistedInject constructor(
)
}.onRight {
Timber.d("Successful hash submission")
if (transaction.type != StakingTransactionType.FREEZE_ENERGY) {
savePendingTransactionUseCase.invoke(userWallet.walletId, pendingTransaction)
}
}
}
}
@ -263,7 +245,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
private fun getAmount(amountState: AmountState.Data, fee: Fee, reduceAmountBy: BigDecimal?): BigDecimal {
val amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value")
val feeValue = fee.amount.value ?: error("No fee value")
val isEnterAction = stateController.value.actionType == StakingActionCommonType.ENTER
val isEnterAction = stateController.value.actionType == StakingActionCommonType.Enter
return checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isAmountSubtractAvailable && isEnterAction,
@ -274,23 +256,6 @@ internal class StakingTransactionSender @AssistedInject constructor(
)
}
private fun composeStakeTransaction(confirmationState: StakingStates.ConfirmationState.Data): PendingTransaction {
val state = stateController.value
val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data
val token = yield.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId)
return PendingTransaction(
groupId = UUID.randomUUID().toString(),
token = token,
type = BalanceType.STAKED,
amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO,
rawCurrencyId = cryptoCurrencyStatus.currency.id.rawCurrencyId,
validator = (confirmationState.validatorState as? ValidatorState.Content)?.chosenValidator,
balancesId = yieldBalance?.getBalancesUniqueId() ?: 0,
)
}
private data class FullTransactionData(
val stakeKitTransaction: StakingTransaction,
val tangemTransaction: TransactionData.Compiled,

View file

@ -7,8 +7,6 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.*
import kotlinx.collections.immutable.persistentListOf
@ -16,48 +14,6 @@ import java.math.BigDecimal
internal object ConfirmationStatePreviewData {
private val validatorList = listOf(
Yield.Validator(
address = "0xa6e768fef2d1af36c0cfdb276422e7881a83e951",
status = ValidatorStatus.ACTIVE,
name = "Luganodes",
image = "https://assets.stakek.it/validators/luganodes.png",
apr = BigDecimal("0.054823398040640445"),
commission = 0.1,
stakedBalance = "355544384.45009977",
website = "https://luganodes.com/",
votingPower = 0.09778360195377911,
preferred = true,
isStrategicPartner = false,
),
Yield.Validator(
address = "0x35b1ca0f398905cf752e6fe122b51c88022fca32",
status = ValidatorStatus.ACTIVE,
name = "InfStones",
image = "https://assets.stakek.it/validators/infstones.png",
apr = BigDecimal("0.057786472172836965"),
commission = 0.05,
stakedBalance = "12495684.05643019",
website = "https://infstones.com/",
votingPower = 0.0034366257754399774,
preferred = true,
isStrategicPartner = false,
),
Yield.Validator(
address = "0xd14a87025109013b0a2354a775cb335f926af65a",
status = ValidatorStatus.ACTIVE,
name = "Kiln",
image = "https://assets.stakek.it/validators/kiln.png",
apr = BigDecimal("0.057786472172836965"),
commission = 0.05,
stakedBalance = "85400369.96393165",
website = "https://infstones.com/",
votingPower = 0.023487238579718264,
preferred = true,
isStrategicPartner = true,
),
)
private val fee = Fee.Common(
amount = Amount(
currencySymbol = "MATIC",
@ -77,11 +33,6 @@ internal object ConfirmationStatePreviewData {
isFeeApproximate = false,
isFeeConvertibleToFiat = true,
),
validatorState = ValidatorState.Content(
isClickable = true,
chosenValidator = validatorList[0],
availableValidators = validatorList,
),
footerText = stringReference("You stake \$715.11 and will be receiving ~\$35 monthly"),
notifications = persistentListOf(
StakingNotification.Info.EarnRewards(
@ -96,6 +47,5 @@ internal object ConfirmationStatePreviewData {
isApprovalNeeded = false,
reduceAmountBy = null,
pendingActions = null,
possiblePendingTransaction = null,
)
}

View file

@ -0,0 +1,60 @@
package com.tangem.features.staking.impl.presentation.state.previewdata
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus
import com.tangem.features.staking.impl.presentation.state.StakingStates
import java.math.BigDecimal
internal object ValidatorStatePreviewData {
private val validatorList = listOf(
Yield.Validator(
address = "0xa6e768fef2d1af36c0cfdb276422e7881a83e951",
status = ValidatorStatus.ACTIVE,
name = "Luganodes",
image = "https://assets.stakek.it/validators/luganodes.png",
apr = BigDecimal("0.054823398040640445"),
commission = 0.1,
stakedBalance = "355544384.45009977",
website = "https://luganodes.com/",
votingPower = 0.09778360195377911,
preferred = true,
isStrategicPartner = false,
),
Yield.Validator(
address = "0x35b1ca0f398905cf752e6fe122b51c88022fca32",
status = ValidatorStatus.ACTIVE,
name = "InfStones",
image = "https://assets.stakek.it/validators/infstones.png",
apr = BigDecimal("0.057786472172836965"),
commission = 0.05,
stakedBalance = "12495684.05643019",
website = "https://infstones.com/",
votingPower = 0.0034366257754399774,
preferred = true,
isStrategicPartner = true,
),
Yield.Validator(
address = "0xd14a87025109013b0a2354a775cb335f926af65a",
status = ValidatorStatus.ACTIVE,
name = "Kiln",
image = "https://assets.stakek.it/validators/kiln.png",
apr = BigDecimal("0.057786472172836965"),
commission = 0.05,
stakedBalance = "85400369.96393165",
website = "https://infstones.com/",
votingPower = 0.023487238579718264,
preferred = true,
isStrategicPartner = false,
),
)
val validatorState = StakingStates.ValidatorState.Data(
availableValidators = validatorList,
chosenValidator = validatorList.first(),
isPrimaryButtonEnabled = true,
activeValidator = null,
isClickable = true,
isVisibleOnConfirmation = true,
)
}

View file

@ -2,14 +2,11 @@ package com.tangem.features.staking.impl.presentation.state.stub
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
@Suppress("TooManyFunctions")
@ -17,13 +14,7 @@ internal object StakingClickIntentsStub : StakingClickIntents {
override fun onBackClick() {}
override fun onNextClick(
actionTypeToOverwrite: StakingActionCommonType?,
pendingAction: PendingAction?,
pendingActions: ImmutableList<PendingAction>?,
balanceState: BalanceState?,
) {
}
override fun onNextClick(balanceState: BalanceState?) {}
override fun onActionClick() {}
@ -67,8 +58,7 @@ internal object StakingClickIntentsStub : StakingClickIntents {
override fun onActiveStake(activeStake: BalanceState) {}
override fun getFee(pendingAction: PendingAction?, pendingActions: ImmutableList<PendingAction>?) {
}
override fun getFee() {}
override fun onAmountReduceByClick(
reduceAmountBy: BigDecimal,

View file

@ -1,33 +0,0 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.domain.staking.model.stakekit.BalanceType
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Network
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.lib.crypto.BlockchainUtils.isTron
import com.tangem.utils.transformer.Transformer
internal class ActionTypeActiveStakeTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val activeStake: BalanceState,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val isTronStakedBalance = isTronStakedBalance(
networkId = cryptoCurrencyStatus.currency.network.id,
activeStake = activeStake,
)
val actionType = if (activeStake.pendingActions.isEmpty() || isTronStakedBalance) {
StakingActionCommonType.EXIT
} else {
StakingActionCommonType.PENDING_OTHER
}
return prevState.copy(actionType = actionType)
}
private fun isTronStakedBalance(networkId: Network.ID, activeStake: BalanceState): Boolean {
return isTron(networkId.value) && activeStake.type == BalanceType.STAKED
}
}

View file

@ -65,7 +65,7 @@ internal class AddStakingNotificationsTransformer(
val feeValue = feeState?.fee?.amount?.value.orZero()
val reduceAmountBy = confirmationState.reduceAmountBy.orZero()
val isEnterAction = prevState.actionType == StakingActionCommonType.ENTER
val isEnterAction = prevState.actionType == StakingActionCommonType.Enter
val isFeeCoverage = checkFeeCoverage(
amountValue = amountValue,
feeValue = feeValue,
@ -93,12 +93,7 @@ internal class AddStakingNotificationsTransformer(
prevState = prevState,
feeError = feeError,
sendingAmount = sendingAmount,
onReload = {
prevState.clickIntents.getFee(
confirmationState.pendingAction,
confirmationState.pendingActions,
)
},
onReload = prevState.clickIntents::getFee,
feeValue = feeValue,
)
// warnings
@ -228,7 +223,7 @@ internal class AddStakingNotificationsTransformer(
val showNotification = sendingAmount + feeAmount > balance
if (showNotification) {
val notification = if (actionType == StakingActionCommonType.ENTER) {
val notification = if (actionType == StakingActionCommonType.Enter) {
NotificationUM.Error.TotalExceedsBalance
} else {
with(cryptoCurrencyStatus.currency) {
@ -249,9 +244,9 @@ internal class AddStakingNotificationsTransformer(
private fun MutableList<NotificationUM>.addInfoNotifications(prevState: StakingUiState) {
when (prevState.actionType) {
StakingActionCommonType.EXIT -> addExitInfoNotifications()
StakingActionCommonType.ENTER -> addEnterInfoNotifications()
else -> addPendingInfoNotifications(prevState)
StakingActionCommonType.Enter -> addEnterInfoNotifications()
StakingActionCommonType.Exit -> addExitInfoNotifications()
is StakingActionCommonType.Pending -> addPendingInfoNotifications(prevState)
}
}
@ -312,6 +307,10 @@ internal class AddStakingNotificationsTransformer(
resourceReference(R.string.staking_revote) to
resourceReference(R.string.staking_notifications_revote_tron_text)
}
StakingActionType.RESTAKE -> {
resourceReference(R.string.staking_restake) to
resourceReference(R.string.staking_notification_restake_text)
}
else -> null to null
}

View file

@ -1,25 +0,0 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
internal class SetActionToExecuteTransformer(
private val actionTypeToOverwrite: StakingActionCommonType,
private val pendingAction: PendingAction?,
private val pendingActions: ImmutableList<PendingAction>?,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
return prevState.copy(
actionType = actionTypeToOverwrite,
confirmationState = confirmationState?.copy(
pendingAction = pendingAction,
pendingActions = pendingActions,
) ?: StakingStates.ConfirmationState.Empty(),
)
}
}

View file

@ -109,6 +109,7 @@ internal class SetButtonsStateTransformer(
StakingStep.Confirmation -> getConfirmationButtonText()
StakingStep.Validators -> resourceReference(R.string.common_continue)
StakingStep.Amount,
StakingStep.RestakeValidator,
StakingStep.RewardsValidators,
-> resourceReference(R.string.common_next)
}
@ -120,17 +121,15 @@ internal class SetButtonsStateTransformer(
resourceReference(R.string.common_close)
} else {
when (actionType) {
StakingActionCommonType.ENTER -> {
StakingActionCommonType.Enter -> {
if (confirmationState.isApprovalNeeded) {
resourceReference(R.string.give_permission_title)
} else {
resourceReference(R.string.common_stake)
}
}
StakingActionCommonType.EXIT -> resourceReference(R.string.common_unstake)
StakingActionCommonType.PENDING_OTHER,
StakingActionCommonType.PENDING_REWARDS,
-> confirmationState.pendingAction?.type.getPendingActionTitle()
StakingActionCommonType.Exit -> resourceReference(R.string.common_unstake)
is StakingActionCommonType.Pending -> confirmationState.pendingAction?.type.getPendingActionTitle()
}
}
} else {
@ -140,18 +139,11 @@ internal class SetButtonsStateTransformer(
private fun StakingUiState.onPrimaryClick() {
when (currentStep) {
StakingStep.InitialInfo -> clickIntents.onEnterClick()
StakingStep.Validators -> {
if (confirmationState is StakingStates.ConfirmationState.Data) {
clickIntents.onNextClick(
pendingActions = confirmationState.pendingActions,
pendingAction = confirmationState.pendingAction,
)
} else {
clickIntents.onNextClick()
}
}
StakingStep.Amount -> clickIntents.onNextClick()
StakingStep.InitialInfo -> clickIntents.onNextClick()
StakingStep.Validators,
StakingStep.RestakeValidator,
-> clickIntents.onNextClick()
StakingStep.Amount -> clickIntents.onEnterClick()
StakingStep.Confirmation -> onConfirmationClick()
StakingStep.RewardsValidators -> Unit
}
@ -162,7 +154,7 @@ internal class SetButtonsStateTransformer(
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
clickIntents.onNextClick()
} else {
val isEnterAction = actionType == StakingActionCommonType.ENTER
val isEnterAction = actionType == StakingActionCommonType.Enter
val isApproveNeeded = confirmationState.isApprovalNeeded
if (isEnterAction && isApproveNeeded) {
@ -179,6 +171,7 @@ internal class SetButtonsStateTransformer(
private fun StakingStep.isPrevButtonVisible(): Boolean = when (this) {
StakingStep.InitialInfo,
StakingStep.RewardsValidators,
StakingStep.RestakeValidator,
StakingStep.Confirmation,
StakingStep.Validators,
-> false
@ -192,7 +185,9 @@ internal class SetButtonsStateTransformer(
StakingStep.Amount -> amountState.isPrimaryButtonEnabled
StakingStep.Confirmation -> confirmationState.isPrimaryButtonEnabled
StakingStep.RewardsValidators -> rewardsValidatorsState.isPrimaryButtonEnabled
StakingStep.Validators -> true
StakingStep.RestakeValidator,
StakingStep.Validators,
-> true
}
}
}

View file

@ -2,22 +2,17 @@ package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.FeeState
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.Provider
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
internal class SetConfirmationStateAssentTransformer(
private val appCurrencyProvider: Provider<AppCurrency>,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
private val fee: Fee,
private val action: PendingAction?,
private val actions: ImmutableList<PendingAction>?,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
@ -30,7 +25,6 @@ internal class SetConfirmationStateAssentTransformer(
if (this is StakingStates.ConfirmationState.Data) {
val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true
return copy(
innerState = InnerConfirmationStakingState.ASSENT,
feeState = FeeState.Content(
fee = fee,
rate = feeCryptoCurrencyStatus?.value?.fiatRate,
@ -38,11 +32,7 @@ internal class SetConfirmationStateAssentTransformer(
appCurrency = appCurrencyProvider(),
isFeeApproximate = false,
),
validatorState = validatorState.copySealed(isClickable = true),
pendingAction = action,
pendingActions = actions,
isPrimaryButtonEnabled = true,
isApprovalNeeded = false,
)
} else {
return this

Some files were not shown because too many files have changed in this diff Show more