Updated on 2026-08-14
This commit is contained in:
commit
d0b35bc331
680 changed files with 26556 additions and 2709 deletions
|
|
@ -112,6 +112,7 @@ dependencies {
|
|||
implementation(projects.features.sendV2.api)
|
||||
implementation(projects.features.tokenRecieve.api)
|
||||
implementation(projects.features.yieldSupply.api)
|
||||
implementation(projects.features.commonFeatures.api)
|
||||
|
||||
implementation(deps.decompose.ext.compose)
|
||||
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDeta
|
|||
import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDetailsBottomSheetConfig
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreenLegacy
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.AddFundsBottomSheetComponent
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.ChooseAddressBottomSheetComponent
|
||||
import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.CloreMigrationBottomSheetComponent
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.DynamicAddressesBottomSheetComponent
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.TransferBottomSheetComponent
|
||||
|
|
@ -47,6 +47,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
|
|||
expressTransactionsComponentFactory: ExpressTransactionsComponent.Factory,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
private val yieldSupplyWarningComponentFactory: YieldSupplyDepositedWarningComponent.Factory,
|
||||
private val addFundsComponentFactory: AddFundsComponent.Factory,
|
||||
yieldSupplyComponentFactory: YieldSupplyComponent.Factory,
|
||||
private val ratingComponentFactory: RatingComponent.Factory,
|
||||
) : TokenDetailsComponent, AppComponentContext by appComponentContext {
|
||||
|
|
@ -177,9 +178,15 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
|
|||
dynamicAddressesDelegate = model.dynamicAddressesDelegate,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
)
|
||||
is TokenDetailsBottomSheetConfig.AddFunds -> AddFundsBottomSheetComponent(
|
||||
stateFlow = model.addFundsUiState,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
is TokenDetailsBottomSheetConfig.AddFunds -> addFundsComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = AddFundsComponent.Params(
|
||||
launchMode = AddFundsComponent.LaunchMode.TokenActionsOnly(
|
||||
userWalletId = route.userWalletId,
|
||||
currency = route.currency,
|
||||
),
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
is TokenDetailsBottomSheetConfig.Transfer -> TransferBottomSheetComponent(
|
||||
stateFlow = model.transferUiState,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.feature.tokendetails
|
||||
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.tokendetails.TokenDetailsFeatureToggles
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultTokenDetailsFeatureToggles @Inject constructor(
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
) : TokenDetailsFeatureToggles {
|
||||
|
||||
override val isQuickTopUpEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.AND_15258_QUICK_TOP_UP_ENABLED,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.feature.tokendetails.di
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.feature.tokendetails.DefaultTokenDetailsFeatureToggles
|
||||
import com.tangem.features.tokendetails.TokenDetailsFeatureToggles
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object TokenDetailsFeatureModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTokenDetailsFeatureToggles(featureTogglesManager: FeatureTogglesManager): TokenDetailsFeatureToggles {
|
||||
return DefaultTokenDetailsFeatureToggles(featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
interface TokenDetailsClickIntents {
|
||||
|
|
@ -71,6 +72,8 @@ interface TokenDetailsClickIntents {
|
|||
|
||||
fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig)
|
||||
|
||||
fun onQuickTopUpClick(amount: BigDecimal, currencyCode: String)
|
||||
|
||||
fun onYieldInfoClick()
|
||||
|
||||
// region Clore migration
|
||||
|
|
@ -168,6 +171,8 @@ internal class EmptyTokenDetailsClickIntents : TokenDetailsClickIntents {
|
|||
|
||||
override fun onYieldInfoClick() { /* no op */ }
|
||||
|
||||
override fun onQuickTopUpClick(amount: BigDecimal, currencyCode: String) { /* no op */ }
|
||||
|
||||
override fun onCopyAddress(): TextReference? {
|
||||
/* no op */
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ import com.tangem.domain.models.network.NetworkAddress
|
|||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.offramp.GetOfframpUrlUseCase
|
||||
import com.tangem.domain.onramp.CheckOnrampAvailabilityUseCase
|
||||
import com.tangem.domain.onramp.model.OnrampSource
|
||||
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
|
||||
import com.tangem.domain.staking.GetStakingEntryInfoUseCase
|
||||
|
|
@ -84,6 +85,7 @@ import com.tangem.domain.transaction.error.OpenTrustlineError
|
|||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.domain.transaction.usecase.*
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletIconUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase
|
||||
|
|
@ -103,6 +105,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta
|
|||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsStateController
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.QuickTopUpBlockFactory
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.InitializeWithCryptoCurrencyTransformer
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.SetBalanceTransformer
|
||||
|
|
@ -132,6 +135,7 @@ import kotlinx.coroutines.async
|
|||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass", "TooManyFunctions", "PropertyUsedBeforeDeclaration")
|
||||
|
|
@ -192,6 +196,9 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
private val redesignStateController: TokenDetailsStateController,
|
||||
private val swapFeedbackUseCase: SwapFeedbackUseCase,
|
||||
private val swapFeatureToggles: SwapFeatureToggles,
|
||||
private val quickTopUpBlockFactory: QuickTopUpBlockFactory,
|
||||
private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
|
||||
private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase,
|
||||
) : Model(),
|
||||
TokenDetailsClickIntents,
|
||||
YieldSupplyDepositedWarningComponent.ModelCallback {
|
||||
|
|
@ -304,6 +311,7 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
handleBalanceHiding()
|
||||
checkForActionUpdates()
|
||||
handleNavigationParam()
|
||||
observeQuickTopUpBlock()
|
||||
}
|
||||
|
||||
private fun initButtons() {
|
||||
|
|
@ -551,7 +559,12 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onAddFundsClick() {
|
||||
bottomSheetNavigation.activate(TokenDetailsBottomSheetConfig.AddFunds)
|
||||
bottomSheetNavigation.activate(
|
||||
TokenDetailsBottomSheetConfig.AddFunds(
|
||||
userWalletId = userWalletId,
|
||||
currency = cryptoCurrency,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTransferClick() {
|
||||
|
|
@ -584,6 +597,43 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onQuickTopUpClick(amount: BigDecimal, currencyCode: String) {
|
||||
analyticsEventsHandler.send(
|
||||
TokenScreenAnalyticsEvent.ButtonQuickTopUp(
|
||||
token = cryptoCurrency.symbol,
|
||||
blockchain = cryptoCurrency.network.name,
|
||||
currency = currencyCode,
|
||||
value = amount.toInt().toString(),
|
||||
),
|
||||
)
|
||||
appRouter.push(
|
||||
AppRoute.Onramp(
|
||||
source = OnrampSource.TOKEN_DETAILS,
|
||||
userWalletId = userWalletId,
|
||||
currency = cryptoCurrency,
|
||||
initialFiatAmount = amount,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onQuickTopUpOtherClick() {
|
||||
analyticsEventsHandler.send(
|
||||
TokenScreenAnalyticsEvent.ButtonWithParams.ButtonBuy(
|
||||
token = cryptoCurrency.symbol,
|
||||
blockchain = cryptoCurrency.network.name,
|
||||
status = ScenarioUnavailabilityReason.None.toReasonAnalyticsText(),
|
||||
derivationIndex = getAccountIndexOrNull(),
|
||||
),
|
||||
)
|
||||
appRouter.push(
|
||||
AppRoute.Onramp(
|
||||
source = OnrampSource.TOKEN_DETAILS,
|
||||
userWalletId = userWalletId,
|
||||
currency = cryptoCurrency,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBuyCoinClick(cryptoCurrency: CryptoCurrency) {
|
||||
analyticsEventsHandler.send(
|
||||
TokenScreenAnalyticsEvent.ButtonWithParams.ButtonBuy(
|
||||
|
|
@ -1363,6 +1413,38 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
observeRedesignStakingNotification()
|
||||
}
|
||||
|
||||
private fun observeQuickTopUpBlock() {
|
||||
getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency)
|
||||
.map { it.status }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { status ->
|
||||
flow {
|
||||
val amount = status.value.amount
|
||||
if (amount == null || !amount.isZero()) {
|
||||
emit(null)
|
||||
return@flow
|
||||
}
|
||||
val txCount = getTxHistoryItemsCountUseCase(userWalletId, cryptoCurrency)
|
||||
val availability = checkOnrampAvailabilityUseCase(userWallet)
|
||||
emit(
|
||||
quickTopUpBlockFactory.build(
|
||||
currencyStatus = status,
|
||||
isTxHistoryEmpty = txCount,
|
||||
onrampAvailability = availability,
|
||||
onPresetClick = ::onQuickTopUpClick,
|
||||
onOtherClick = ::onQuickTopUpOtherClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
.onEach { block ->
|
||||
uiState.value = uiState.value.copy(quickTopUpBlock = block)
|
||||
redesignStateController.update { state -> state.copy(quickTopUpBlock = block) }
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun observeRedesignStakingNotification() {
|
||||
val statusFlow = getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency)
|
||||
.map { it.status }
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.tangem.core.decompose.navigation.Route
|
|||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokens.model.details.TokenAction
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -32,7 +33,10 @@ sealed class TokenDetailsBottomSheetConfig : Route {
|
|||
data object DynamicAddresses : TokenDetailsBottomSheetConfig()
|
||||
|
||||
@Serializable
|
||||
data object AddFunds : TokenDetailsBottomSheetConfig()
|
||||
data class AddFunds(
|
||||
val userWalletId: UserWalletId,
|
||||
val currency: CryptoCurrency,
|
||||
) : TokenDetailsBottomSheetConfig()
|
||||
|
||||
@Serializable
|
||||
data object Transfer : TokenDetailsBottomSheetConfig()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal data class QuickTopUpBlockUM(
|
||||
val amounts: ImmutableList<QuickTopUpAmountUM>,
|
||||
) {
|
||||
@Immutable
|
||||
data class QuickTopUpAmountUM(
|
||||
val displayValue: TextReference,
|
||||
val onClick: () -> Unit,
|
||||
val isOther: Boolean = false,
|
||||
)
|
||||
}
|
||||
|
|
@ -15,4 +15,5 @@ internal data class TokenDetailsState(
|
|||
val pullToRefreshConfig: PullToRefreshConfig,
|
||||
val isBalanceHidden: Boolean,
|
||||
val isMarketPriceAvailable: Boolean,
|
||||
val quickTopUpBlock: QuickTopUpBlockUM? = null,
|
||||
)
|
||||
|
|
@ -24,6 +24,7 @@ internal data class TokenDetailsUM(
|
|||
val addFundsUM: AddFundsUM,
|
||||
val transferUM: TransferUM,
|
||||
val zeroBalanceActionsUM: ZeroBalanceActionsUM,
|
||||
val quickTopUpBlock: QuickTopUpBlockUM? = null,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.res.R
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.onramp.model.OnrampAvailability
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.domain.txhistory.models.TxHistoryStateError
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.QuickTopUpBlockUM
|
||||
import com.tangem.features.tokendetails.TokenDetailsFeatureToggles
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class QuickTopUpBlockFactory @Inject constructor(
|
||||
private val featureToggles: TokenDetailsFeatureToggles,
|
||||
) {
|
||||
|
||||
fun build(
|
||||
currencyStatus: CryptoCurrencyStatus,
|
||||
isTxHistoryEmpty: Either<TxHistoryStateError, Int>,
|
||||
onrampAvailability: Either<OnrampError, OnrampAvailability>,
|
||||
onPresetClick: (BigDecimal, String) -> Unit,
|
||||
onOtherClick: () -> Unit,
|
||||
): QuickTopUpBlockUM? {
|
||||
if (!featureToggles.isQuickTopUpEnabled) return null
|
||||
|
||||
val amount = currencyStatus.value.amount
|
||||
if (amount == null || !amount.isZero()) return null
|
||||
|
||||
val isHistoryEmpty = isTxHistoryEmpty.fold(
|
||||
ifLeft = { it is TxHistoryStateError.EmptyTxHistories },
|
||||
ifRight = { it == 0 },
|
||||
)
|
||||
if (!isHistoryEmpty) return null
|
||||
|
||||
val currency = when (val availability = onrampAvailability.getOrNull()) {
|
||||
is OnrampAvailability.Available -> availability.currency
|
||||
is OnrampAvailability.ConfirmResidency -> {
|
||||
if (!availability.country.onrampAvailable) return null
|
||||
availability.country.defaultCurrency
|
||||
}
|
||||
else -> return null
|
||||
}
|
||||
|
||||
val presets = when (currency.code) {
|
||||
USD_CODE -> USD_PRESETS
|
||||
EUR_CODE -> EUR_PRESETS
|
||||
else -> return null
|
||||
}
|
||||
|
||||
val presetAmounts = presets.map { value ->
|
||||
QuickTopUpBlockUM.QuickTopUpAmountUM(
|
||||
displayValue = stringReference("${currency.unit}$value"),
|
||||
onClick = { onPresetClick(BigDecimal(value), currency.code) },
|
||||
)
|
||||
}
|
||||
val otherAmount = QuickTopUpBlockUM.QuickTopUpAmountUM(
|
||||
displayValue = resourceReference(R.string.quick_top_up_chip_other),
|
||||
onClick = onOtherClick,
|
||||
isOther = true,
|
||||
)
|
||||
|
||||
return QuickTopUpBlockUM(
|
||||
amounts = (presetAmounts + otherAmount).toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val USD_CODE = "USD"
|
||||
const val EUR_CODE = "EUR"
|
||||
|
||||
val USD_PRESETS = listOf(50, 200, 700)
|
||||
val EUR_PRESETS = listOf(50, 200, 650)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,12 +8,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
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.defaultAmount
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.formatStyled
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.format.bigdecimal.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
|
|
@ -61,12 +56,12 @@ internal class UpdateStakingNotificationTransformer(
|
|||
iconUM = EarnBlockUM.IconUM.Plain(iconRes = CoreUiR.drawable.ic_staking_disable_40),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(CoreResR.string.common_staking),
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
style = EarnBlockUM.TitleUM.Style.Small,
|
||||
tone = EarnBlockUM.TitleUM.Tone.Disabled,
|
||||
),
|
||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||
text = resourceReference(CoreResR.string.staking_notification_network_error_text),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
|
||||
),
|
||||
trailingUM = null,
|
||||
|
|
@ -122,12 +117,12 @@ internal class UpdateStakingNotificationTransformer(
|
|||
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = CoreUiR.drawable.ic_staking_40),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(id = CoreResR.string.common_staking),
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
style = EarnBlockUM.TitleUM.Style.Small,
|
||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||
),
|
||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||
text = stakeAvailableSubtitle(availability.option.displayRewardInfo),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
|
||||
),
|
||||
trailingUM = EarnBlockUM.TrailingUM.Button(
|
||||
|
|
@ -170,7 +165,7 @@ internal class UpdateStakingNotificationTransformer(
|
|||
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = CoreUiR.drawable.ic_staking_40),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(CoreResR.string.staking_enabled),
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
style = EarnBlockUM.TitleUM.Style.Small,
|
||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||
),
|
||||
subtitleUM = getRewardSubtitle(status, rewardFiatAmount),
|
||||
|
|
@ -257,7 +252,7 @@ internal class UpdateStakingNotificationTransformer(
|
|||
|
||||
return EarnBlockUM.SubtitleUM.Text(
|
||||
text = text,
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||
tone = if (isAccent) EarnBlockUM.SubtitleUM.Tone.Accent else EarnBlockUM.SubtitleUM.Tone.Disabled,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.res.R
|
||||
import com.tangem.core.ui.extensions.combinedReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.QuickTopUpBlockUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
private val quickTopUpGradientBrush = Brush.linearGradient(
|
||||
colors = listOf(Color(0xFFEDE5F3), Color(0xFFD7EDD9)),
|
||||
start = Offset(0f, 0f),
|
||||
end = Offset(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY),
|
||||
)
|
||||
|
||||
private val quickTopUpBorderBrush = Brush.sweepGradient(
|
||||
listOf(
|
||||
Color(0x0D000000),
|
||||
Color(0x26000000),
|
||||
Color(0x0D000000),
|
||||
Color(0x26000000),
|
||||
),
|
||||
)
|
||||
|
||||
@Composable
|
||||
internal fun QuickTopUpBlock(state: QuickTopUpBlockUM, modifier: Modifier = Modifier) {
|
||||
val shape = RoundedCornerShape(TangemTheme.dimens.radius20)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape)
|
||||
.background(brush = quickTopUpGradientBrush)
|
||||
.border(width = 1.dp, brush = quickTopUpBorderBrush, shape = shape),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
val textColor = TangemTheme.colors.text.primary1
|
||||
Text(
|
||||
text = combinedReference(
|
||||
stringReference("${StringsSigns.LIGHTNING} "),
|
||||
resourceReference(R.string.quick_top_up_title),
|
||||
).resolveReference(),
|
||||
style = TangemTheme.typography.subtitle1.copy(fontWeight = FontWeight.SemiBold),
|
||||
modifier = Modifier.graphicsLayer {
|
||||
colorFilter = ColorFilter.tint(textColor, BlendMode.SrcIn)
|
||||
},
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6),
|
||||
) {
|
||||
state.amounts.fastForEach { amountUM ->
|
||||
Button(
|
||||
onClick = amountUM.onClick,
|
||||
shape = CircleShape,
|
||||
contentPadding = PaddingValues(
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
vertical = TangemTheme.dimens.spacing0,
|
||||
),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
contentColor = TangemTheme.colors.text.primary1,
|
||||
),
|
||||
modifier = Modifier.heightIn(TangemTheme.dimens.size36),
|
||||
elevation = null,
|
||||
) {
|
||||
Text(
|
||||
text = amountUM.displayValue.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle1.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Composable
|
||||
private fun QuickTopUpBlock_Preview() {
|
||||
TangemThemePreviewRedesign {
|
||||
QuickTopUpBlock(
|
||||
state = QuickTopUpBlockUM(
|
||||
amounts = persistentListOf(
|
||||
QuickTopUpBlockUM.QuickTopUpAmountUM(
|
||||
displayValue = stringReference("\$50"),
|
||||
onClick = {},
|
||||
),
|
||||
QuickTopUpBlockUM.QuickTopUpAmountUM(
|
||||
displayValue = stringReference("\$200"),
|
||||
onClick = {},
|
||||
),
|
||||
QuickTopUpBlockUM.QuickTopUpAmountUM(
|
||||
displayValue = stringReference("\$700"),
|
||||
onClick = {},
|
||||
),
|
||||
QuickTopUpBlockUM.QuickTopUpAmountUM(
|
||||
displayValue = resourceReference(R.string.quick_top_up_chip_other),
|
||||
onClick = {},
|
||||
isOther = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,60 +2,47 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import com.tangem.common.ui.earn.EarnBlock
|
||||
import com.tangem.common.ui.notifications.notifications
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.common.ui.account.AccountIconUM
|
||||
import com.tangem.common.ui.earn.EarnBlock
|
||||
import com.tangem.common.ui.earn.EarnBlockUM
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState
|
||||
import com.tangem.common.ui.notifications.notifications
|
||||
import com.tangem.core.ui.components.BottomFade
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
|
||||
import com.tangem.core.ui.ds.button.TangemButtonType
|
||||
import com.tangem.core.ui.ds.button.TangemButtonUM
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshSlidingContainer
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
|
||||
import com.tangem.core.ui.components.haze.hazeSourceTangem
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.topFade
|
||||
import com.tangem.core.ui.ds.button.TangemButtonType
|
||||
import com.tangem.core.ui.ds.button.TangemButtonUM
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.themedColor
|
||||
import com.tangem.core.ui.res.LocalHazeState
|
||||
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.ZeroBalanceActionsBlock
|
||||
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
|
||||
|
|
@ -64,6 +51,7 @@ import com.tangem.features.txhistory.component.TxHistoryComponent
|
|||
import com.tangem.features.txhistory.entity.TxHistoryItemsUM
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
import com.tangem.features.yield.supply.api.YieldSupplyComponent
|
||||
import dev.chrisbanes.haze.rememberHazeState
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -72,6 +60,9 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
private val TopBarHeight: Dp = 64.dp
|
||||
private val MarketBlockHorizontalPadding: Dp = 14.dp
|
||||
|
||||
private const val TOP_FADE_MID_STOP = 0.8f
|
||||
private const val TOP_FADE_MID_ALPHA = 0.8f
|
||||
|
||||
@Composable
|
||||
internal fun TokenDetailsScreen(
|
||||
tokenDetailsUM: TokenDetailsUM,
|
||||
|
|
@ -84,20 +75,17 @@ internal fun TokenDetailsScreen(
|
|||
val expressState by expressTransactionsComponent.state.collectAsStateWithLifecycle()
|
||||
val statusBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() }
|
||||
val topBarTotalHeight = TopBarHeight + statusBarHeight
|
||||
val hazeState = rememberHazeState()
|
||||
|
||||
val rootBackground = TangemTheme.colors2.surface.level2
|
||||
var marketBlockHeight by remember { mutableStateOf(0.dp) }
|
||||
val effectiveBottomPadding = marketBlockHeight + TangemTheme.dimens2.x4
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(rootBackground),
|
||||
) {
|
||||
CompositionLocalProvider(LocalHazeState provides hazeState) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.hazeSourceTangem(zIndex = -2f),
|
||||
.background(rootBackground),
|
||||
) {
|
||||
TangemPullToRefreshSlidingContainer(
|
||||
config = tokenDetailsUM.pullToRefreshConfig,
|
||||
|
|
@ -115,27 +103,17 @@ internal fun TokenDetailsScreen(
|
|||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
TokenDetailsTopBar(topAppBarUM = tokenDetailsUM.topAppBarUM)
|
||||
|
||||
if (tokenMarketBlockComponent != null) {
|
||||
TokenDetailsMarketBlockOverlay(
|
||||
component = tokenMarketBlockComponent,
|
||||
onHeightChange = { marketBlockHeight = it },
|
||||
)
|
||||
}
|
||||
expressState.bottomSheetSlot?.content(null)
|
||||
}
|
||||
|
||||
TokenDetailsTopBarOverlay(topAppBarUM = tokenDetailsUM.topAppBarUM)
|
||||
|
||||
if (tokenMarketBlockComponent != null) {
|
||||
TokenDetailsMarketBlockOverlay(
|
||||
component = tokenMarketBlockComponent,
|
||||
onHeightChange = { marketBlockHeight = it },
|
||||
)
|
||||
}
|
||||
|
||||
expressState.bottomSheetSlot?.content(null)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenDetailsTopBarOverlay(topAppBarUM: TokenDetailsTopAppBarUM) {
|
||||
Box(
|
||||
modifier = Modifier.background(TangemTheme.colors2.surface.level2),
|
||||
) {
|
||||
TokenDetailsTopBar(topAppBarUM = topAppBarUM)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -165,7 +143,7 @@ private fun BoxScope.TokenDetailsMarketBlockOverlay(
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
@Composable
|
||||
private fun TokenDetailsBody(
|
||||
tokenDetailsUM: TokenDetailsUM,
|
||||
|
|
@ -189,7 +167,15 @@ private fun TokenDetailsBody(
|
|||
.padding(start = TangemTheme.dimens2.x4, end = TangemTheme.dimens2.x4, top = TangemTheme.dimens2.x4)
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
modifier = modifier
|
||||
.testTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER)
|
||||
.hazeSourceTangem(state = LocalHazeState.current)
|
||||
.topFade(
|
||||
height = topContentPadding,
|
||||
0f to rootBackground,
|
||||
TOP_FADE_MID_STOP to rootBackground.copy(alpha = TOP_FADE_MID_ALPHA),
|
||||
1f to Color.Transparent,
|
||||
),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(top = topContentPadding, bottom = bottomContentPadding),
|
||||
) {
|
||||
|
|
@ -201,14 +187,6 @@ private fun TokenDetailsBody(
|
|||
)
|
||||
}
|
||||
val balance = tokenDetailsUM.balanceBlockUM
|
||||
if (balance is TokenDetailsBalanceBlockUM.Content && balance.isBalanceZero) {
|
||||
item(key = "zero_balance_actions") {
|
||||
ZeroBalanceActionsBlock(
|
||||
state = tokenDetailsUM.zeroBalanceActionsUM,
|
||||
modifier = itemModifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
notifications(
|
||||
notifications = tokenDetailsUM.notifications,
|
||||
contentColor = rootBackground,
|
||||
|
|
@ -216,14 +194,31 @@ private fun TokenDetailsBody(
|
|||
)
|
||||
tokenDetailsUM.earnBlockState?.let { earnBlock ->
|
||||
item(key = "staking_block") {
|
||||
val stakingTag = when {
|
||||
earnBlock is EarnBlockUM.Content &&
|
||||
earnBlock.type == EarnBlockUM.Type.Staking &&
|
||||
earnBlock.trailingUM is EarnBlockUM.TrailingUM.Button ->
|
||||
TokenDetailsScreenTestTags.STAKING_AVAILABLE_BLOCK
|
||||
else -> TokenDetailsScreenTestTags.STAKING_BLOCK
|
||||
}
|
||||
EarnBlock(
|
||||
state = earnBlock,
|
||||
modifier = itemModifier,
|
||||
modifier = itemModifier
|
||||
.padding(vertical = TangemTheme.dimens2.x3)
|
||||
.testTag(stakingTag),
|
||||
)
|
||||
}
|
||||
}
|
||||
item(key = "yield_supply_block") {
|
||||
yieldSupplyComponent.Content(modifier = itemModifier.padding(vertical = TangemTheme.dimens2.x2))
|
||||
yieldSupplyComponent.Content(modifier = itemModifier.padding(vertical = TangemTheme.dimens2.x3))
|
||||
}
|
||||
if (balance is TokenDetailsBalanceBlockUM.Content && balance.isBalanceZero) {
|
||||
item(key = "zero_balance_actions") {
|
||||
ZeroBalanceActionsBlock(
|
||||
state = tokenDetailsUM.zeroBalanceActionsUM,
|
||||
modifier = itemModifier.padding(vertical = TangemTheme.dimens2.x2),
|
||||
)
|
||||
}
|
||||
}
|
||||
with(expressTransactionsComponent) {
|
||||
expressTransactionsContent(
|
||||
|
|
@ -231,6 +226,14 @@ private fun TokenDetailsBody(
|
|||
modifier = expressTransactionModifier,
|
||||
)
|
||||
}
|
||||
tokenDetailsUM.quickTopUpBlock?.let { quickTopUpBlock ->
|
||||
item(key = "quick_top_up_block") {
|
||||
QuickTopUpBlock(
|
||||
state = quickTopUpBlock,
|
||||
modifier = itemModifier.padding(vertical = TangemTheme.dimens2.x0),
|
||||
)
|
||||
}
|
||||
}
|
||||
with(txHistoryComponent) {
|
||||
txHistoryContent(listState = listState, state = txHistoryState)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,6 +160,15 @@ internal fun TokenDetailsScreenLegacy(
|
|||
)
|
||||
}
|
||||
|
||||
state.quickTopUpBlock?.let { quickTopUpBlock ->
|
||||
item(key = "quick_top_up_block") {
|
||||
QuickTopUpBlock(
|
||||
state = quickTopUpBlock,
|
||||
modifier = itemModifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
with(txHistoryComponent) {
|
||||
txHistoryContentLegacy(listState = listState, state = txHistoryComponentState)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.TextAutoSize
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
|
|
@ -11,11 +13,21 @@ import androidx.compose.runtime.*
|
|||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
|
||||
import com.tangem.core.ui.test.TokenDetailsTopBarTestTags
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
|
@ -30,6 +42,8 @@ import com.tangem.core.ui.components.account.AccountIconSize
|
|||
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem
|
||||
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu
|
||||
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
|
||||
import com.tangem.core.ui.components.haze.ProvideHaze
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.ds.image.DeviceIconUM
|
||||
import com.tangem.core.ui.ds.image.TangemDeviceIcon
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||
|
|
@ -44,19 +58,24 @@ import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
|||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.*
|
||||
import com.tangem.core.ui.R as CoreUiR
|
||||
|
||||
@Composable
|
||||
internal fun TokenDetailsTopBar(topAppBarUM: TokenDetailsTopAppBarUM, modifier: Modifier = Modifier) {
|
||||
val actionModifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.hazeEffectTangem { blurRadius = ACTION_BLUR_RADIUS }
|
||||
TangemTopBar(
|
||||
modifier = modifier.statusBarsPadding(),
|
||||
modifier = modifier
|
||||
.statusBarsPadding()
|
||||
.testTag(TokenDetailsTopBarTestTags.BACK_BUTTON),
|
||||
startContent = {
|
||||
TangemTopBarActionContent(
|
||||
modifier = actionModifier,
|
||||
actionUM = TangemTopBarActionUM(
|
||||
iconRes = R.drawable.ic_arrow_back_28,
|
||||
onClick = topAppBarUM.onBackClick,
|
||||
ghostModeProgress = 1f,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
@ -65,10 +84,10 @@ internal fun TokenDetailsTopBar(topAppBarUM: TokenDetailsTopAppBarUM, modifier:
|
|||
var isDropdownMenuShown by rememberSaveable { mutableStateOf(false) }
|
||||
Box {
|
||||
TangemTopBarActionContent(
|
||||
modifier = actionModifier.testTag(TokenDetailsTopBarTestTags.MORE_BUTTON),
|
||||
actionUM = TangemTopBarActionUM(
|
||||
iconRes = CoreUiR.drawable.ic_more_default_24,
|
||||
onClick = { isDropdownMenuShown = true },
|
||||
ghostModeProgress = 1f,
|
||||
),
|
||||
)
|
||||
TangemDropdownMenu(
|
||||
|
|
@ -97,11 +116,13 @@ internal fun TokenDetailsTopBar(topAppBarUM: TokenDetailsTopAppBarUM, modifier:
|
|||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5),
|
||||
) {
|
||||
TokenDetailsTitle(titleState = topAppBarUM.titleState)
|
||||
Box(modifier = Modifier.testTag(TokenDetailsScreenTestTags.TOKEN_TITLE)) {
|
||||
TokenDetailsTitle(titleState = topAppBarUM.titleState)
|
||||
}
|
||||
Text(
|
||||
text = topAppBarUM.subtitle.resolveAnnotatedReference(),
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = TangemTheme.colors2.text.neutral.tertiary,
|
||||
style = TangemTheme.typography2.captionSemibold12,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
|
@ -138,11 +159,7 @@ private fun TokenDetailsTitle(titleState: TitleState) {
|
|||
AdaptiveTokenWithSecondaryRow(
|
||||
tokenName = titleState.tokenName,
|
||||
secondaryName = titleState.walletName,
|
||||
template = stringResourceSafe(
|
||||
id = CoreUiR.string.token_details_toolbar_title_token_in_wallet,
|
||||
titleState.tokenName,
|
||||
titleState.walletName,
|
||||
),
|
||||
template = formattedTitleTemplate(CoreUiR.string.token_details_toolbar_title_token_in_wallet),
|
||||
appearance = appearance,
|
||||
icon = {
|
||||
TangemDeviceIcon(
|
||||
|
|
@ -153,21 +170,16 @@ private fun TokenDetailsTitle(titleState: TitleState) {
|
|||
)
|
||||
}
|
||||
is TitleState.WithAccount -> {
|
||||
val accountNameStr = titleState.accountName.resolveAnnotatedReference().toString()
|
||||
AdaptiveTokenWithSecondaryRow(
|
||||
tokenName = titleState.tokenName,
|
||||
secondaryName = accountNameStr,
|
||||
template = stringResourceSafe(
|
||||
id = CoreUiR.string.token_details_toolbar_title_token_in_account,
|
||||
titleState.tokenName,
|
||||
accountNameStr,
|
||||
),
|
||||
secondaryName = titleState.accountName.resolveAnnotatedReference().toString(),
|
||||
template = formattedTitleTemplate(CoreUiR.string.token_details_toolbar_title_token_in_account),
|
||||
appearance = appearance,
|
||||
icon = {
|
||||
AccountIcon(
|
||||
name = titleState.accountName,
|
||||
icon = titleState.accountIconUM,
|
||||
size = AccountIconSize.ExtraSmall,
|
||||
size = AccountIconSize.RedesignExtraSmall,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -178,9 +190,12 @@ private fun TokenDetailsTitle(titleState: TitleState) {
|
|||
/**
|
||||
* Adaptive title for [TitleState.WithAccount] / [TitleState.WithWallet].
|
||||
*
|
||||
* Phrase template carries the [IMAGE_PLACEHOLDER] marker — translator decides where
|
||||
* the icon sits (e.g. "Tether in [⭐] Portfolio" or "Tether in My Wallet [⭐]").
|
||||
* RTL is handled by BiDi inside the single [Text].
|
||||
* Raw template carries [TOKEN_MARKER] / [SECONDARY_MARKER] for the names and
|
||||
* [IMAGE_PLACEHOLDER] for the icon — translator decides where each sits
|
||||
* (e.g. "Tether in [⭐] Portfolio" or "Tether in My Wallet [⭐]"). Anything outside
|
||||
* the markers (connecting words, punctuation) is painted as tertiary text — no
|
||||
* locale-specific substring matching required. RTL is handled by BiDi inside the
|
||||
* single [Text].
|
||||
*
|
||||
* Width-driven cascade:
|
||||
* 1–2. Full phrase as single [Text] with inline icon; [TextOverflow.Ellipsis]
|
||||
|
|
@ -201,7 +216,7 @@ private fun AdaptiveTokenWithSecondaryRow(
|
|||
appearance: TitleAppearance,
|
||||
icon: @Composable () -> Unit,
|
||||
) {
|
||||
val (beforeIcon, afterIcon) = remember(template) { splitTemplate(template) }
|
||||
val segments = remember(template) { parseTemplate(template) }
|
||||
val inlineContent = rememberIconInlineContent(appearance.iconSize, icon)
|
||||
|
||||
BoxWithConstraints(
|
||||
|
|
@ -209,16 +224,16 @@ private fun AdaptiveTokenWithSecondaryRow(
|
|||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val isFullTextShown = rememberShouldShowFullText(
|
||||
beforeIcon = beforeIcon,
|
||||
afterIcon = afterIcon,
|
||||
secondaryName = secondaryName,
|
||||
segments = segments,
|
||||
tokenName = tokenName,
|
||||
appearance = appearance,
|
||||
maxWidthPx = constraints.maxWidth,
|
||||
)
|
||||
if (isFullTextShown) {
|
||||
FullPhraseTitle(
|
||||
beforeIcon = beforeIcon,
|
||||
afterIcon = afterIcon,
|
||||
segments = segments,
|
||||
tokenName = tokenName,
|
||||
secondaryName = secondaryName,
|
||||
style = appearance.style,
|
||||
inlineContent = inlineContent,
|
||||
)
|
||||
|
|
@ -232,17 +247,25 @@ private fun AdaptiveTokenWithSecondaryRow(
|
|||
}
|
||||
}
|
||||
|
||||
private fun splitTemplate(template: String): Pair<String, String> {
|
||||
val parts = template.split(IMAGE_PLACEHOLDER, limit = 2)
|
||||
return if (parts.size == 2) parts[0] to parts[1] else template to ""
|
||||
}
|
||||
/**
|
||||
* Returns the title template with its `%1$s` / `%2$s` placeholders replaced by [TOKEN_MARKER] /
|
||||
* [SECONDARY_MARKER] sentinels (and `%%image%%` collapsed to [IMAGE_PLACEHOLDER]).
|
||||
*
|
||||
* Feeding the markers in as format args lets [String.format] resolve all escaping for us, so
|
||||
* [parseTemplate] sees a clean string and the real names stay un-substituted until render time.
|
||||
*/
|
||||
@Composable
|
||||
private fun formattedTitleTemplate(@StringRes id: Int): String = stringResourceSafe(id, TOKEN_MARKER, SECONDARY_MARKER)
|
||||
|
||||
@Composable
|
||||
private fun rememberIconInlineContent(iconSize: Dp, icon: @Composable () -> Unit): Map<String, InlineTextContent> {
|
||||
private fun rememberIconInlineContent(
|
||||
iconSize: Dp,
|
||||
icon: @Composable () -> Unit,
|
||||
): ImmutableMap<String, InlineTextContent> {
|
||||
val iconSizeSp = with(LocalDensity.current) { iconSize.toSp() }
|
||||
val currentIcon by rememberUpdatedState(icon)
|
||||
return remember(iconSizeSp) {
|
||||
mapOf(
|
||||
persistentMapOf(
|
||||
ICON_INLINE_ID to InlineTextContent(
|
||||
placeholder = Placeholder(
|
||||
width = iconSizeSp,
|
||||
|
|
@ -261,23 +284,29 @@ private fun rememberIconInlineContent(iconSize: Dp, icon: @Composable () -> Unit
|
|||
|
||||
@Composable
|
||||
private fun rememberShouldShowFullText(
|
||||
beforeIcon: String,
|
||||
afterIcon: String,
|
||||
secondaryName: String,
|
||||
segments: ImmutableList<TitleSegment>,
|
||||
tokenName: String,
|
||||
appearance: TitleAppearance,
|
||||
maxWidthPx: Int,
|
||||
): Boolean {
|
||||
val measurer = rememberTextMeasurer()
|
||||
val density = LocalDensity.current
|
||||
return remember(beforeIcon, afterIcon, secondaryName, maxWidthPx, appearance, density) {
|
||||
return remember(segments, tokenName, maxWidthPx, appearance, density) {
|
||||
if (maxWidthPx <= 0) return@remember true
|
||||
val fullTextWidthPx = measurer
|
||||
.measure(text = beforeIcon + afterIcon, style = appearance.style, softWrap = false)
|
||||
.size.width
|
||||
val secondaryWidthPx = measurer
|
||||
.measure(text = secondaryName, style = appearance.style, softWrap = false)
|
||||
.size.width
|
||||
val staticWidthPx = (fullTextWidthPx - secondaryWidthPx).coerceAtLeast(0)
|
||||
val staticText = buildString {
|
||||
for (segment in segments) {
|
||||
when (segment) {
|
||||
TitleSegment.Token -> append(tokenName)
|
||||
is TitleSegment.Plain -> append(segment.text)
|
||||
TitleSegment.Secondary, TitleSegment.Image -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
val staticWidthPx = if (staticText.isEmpty()) {
|
||||
0
|
||||
} else {
|
||||
measurer.measure(text = staticText, style = appearance.style, softWrap = false).size.width
|
||||
}
|
||||
val iconReservePx = with(density) {
|
||||
(appearance.iconSize + appearance.spacing * 2).toPx()
|
||||
}.toInt()
|
||||
|
|
@ -288,16 +317,25 @@ private fun rememberShouldShowFullText(
|
|||
|
||||
@Composable
|
||||
private fun FullPhraseTitle(
|
||||
beforeIcon: String,
|
||||
afterIcon: String,
|
||||
segments: ImmutableList<TitleSegment>,
|
||||
tokenName: String,
|
||||
secondaryName: String,
|
||||
style: TextStyle,
|
||||
inlineContent: Map<String, InlineTextContent>,
|
||||
inlineContent: ImmutableMap<String, InlineTextContent>,
|
||||
) {
|
||||
val fullText = remember(beforeIcon, afterIcon) {
|
||||
val tertiaryColor = TangemTheme.colors2.text.neutral.tertiary
|
||||
val fullText = remember(segments, tokenName, secondaryName, tertiaryColor) {
|
||||
buildAnnotatedString {
|
||||
append(beforeIcon)
|
||||
appendInlineContent(ICON_INLINE_ID, IMAGE_PLACEHOLDER)
|
||||
append(afterIcon)
|
||||
for (segment in segments) {
|
||||
when (segment) {
|
||||
TitleSegment.Token -> append(tokenName)
|
||||
TitleSegment.Secondary -> append(secondaryName)
|
||||
TitleSegment.Image -> appendInlineContent(ICON_INLINE_ID, IMAGE_PLACEHOLDER)
|
||||
is TitleSegment.Plain -> withStyle(SpanStyle(color = tertiaryColor)) {
|
||||
append(segment.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(
|
||||
|
|
@ -311,6 +349,47 @@ private fun FullPhraseTitle(
|
|||
)
|
||||
}
|
||||
|
||||
private sealed interface TitleSegment {
|
||||
data object Token : TitleSegment
|
||||
data object Secondary : TitleSegment
|
||||
data object Image : TitleSegment
|
||||
data class Plain(val text: String) : TitleSegment
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the [String.format]ed template (see [formattedTitleTemplate]) into ordered segments.
|
||||
*
|
||||
* Markers appear in their resolved form: [TOKEN_MARKER] / [SECONDARY_MARKER] for the names and
|
||||
* [IMAGE_PLACEHOLDER] for the icon (escaping was already collapsed by [String.format]). Anything
|
||||
* else is plain connecting text painted as tertiary.
|
||||
*/
|
||||
private fun parseTemplate(template: String): ImmutableList<TitleSegment> {
|
||||
val segments = mutableListOf<TitleSegment>()
|
||||
val plain = StringBuilder()
|
||||
|
||||
fun flushPlain() {
|
||||
if (plain.isNotEmpty()) {
|
||||
segments += TitleSegment.Plain(plain.toString())
|
||||
plain.clear()
|
||||
}
|
||||
}
|
||||
|
||||
var i = 0
|
||||
while (i < template.length) {
|
||||
val marker = MARKERS.firstOrNull { (text, _) -> template.startsWith(text, i) }
|
||||
if (marker != null) {
|
||||
flushPlain()
|
||||
segments += marker.second
|
||||
i += marker.first.length
|
||||
} else {
|
||||
plain.append(template[i])
|
||||
i++
|
||||
}
|
||||
}
|
||||
flushPlain()
|
||||
return segments.toImmutableList()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FallbackTokenWithIconTitle(tokenName: String, appearance: TitleAppearance, icon: @Composable () -> Unit) {
|
||||
Row(
|
||||
|
|
@ -342,7 +421,17 @@ private data class TitleAppearance(
|
|||
|
||||
private const val ICON_INLINE_ID = "account_icon"
|
||||
private const val IMAGE_PLACEHOLDER = "%image%"
|
||||
private const val TOKEN_MARKER = "%1\$s"
|
||||
private const val SECONDARY_MARKER = "%2\$s"
|
||||
|
||||
// Order matters: longer/overlapping markers must be tried first by [parseTemplate].
|
||||
private val MARKERS: List<Pair<String, TitleSegment>> = listOf(
|
||||
IMAGE_PLACEHOLDER to TitleSegment.Image,
|
||||
TOKEN_MARKER to TitleSegment.Token,
|
||||
SECONDARY_MARKER to TitleSegment.Secondary,
|
||||
)
|
||||
|
||||
private val ACTION_BLUR_RADIUS = 8.dp
|
||||
private val MIN_TITLE_FONT_SIZE = 12.sp
|
||||
private val MAX_TITLE_FONT_SIZE = 16.sp
|
||||
private val MIN_SECONDARY_NAME_WIDTH = 48.dp
|
||||
|
|
@ -355,20 +444,22 @@ private fun TokenDetailsTopBar_Preview(
|
|||
@PreviewParameter(TokenDetailsTopBarPreviewProvider::class) titleState: TitleState,
|
||||
) {
|
||||
TangemThemePreviewRedesign {
|
||||
TokenDetailsTopBar(
|
||||
topAppBarUM = TokenDetailsTopAppBarUM(
|
||||
titleState = titleState,
|
||||
subtitle = stringReference("ERC-20 in Ethereum network"),
|
||||
onBackClick = {},
|
||||
menuItems = persistentListOf(
|
||||
TangemDropdownMenuItem(
|
||||
title = stringReference("Hide Token"),
|
||||
textColor = themedColor { TangemTheme.colors.text.warning },
|
||||
onClick = {},
|
||||
ProvideHaze {
|
||||
TokenDetailsTopBar(
|
||||
topAppBarUM = TokenDetailsTopAppBarUM(
|
||||
titleState = titleState,
|
||||
subtitle = stringReference("ERC-20 in Ethereum network"),
|
||||
onBackClick = {},
|
||||
menuItems = persistentListOf(
|
||||
TangemDropdownMenuItem(
|
||||
title = stringReference("Hide Token"),
|
||||
textColor = themedColor { TangemTheme.colors.text.warning },
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import com.tangem.core.ui.R as CoreR
|
||||
|
||||
internal class AddFundsBottomSheetComponent(
|
||||
private val stateFlow: StateFlow<AddFundsUM>,
|
||||
private val onDismiss: () -> Unit,
|
||||
) : ComposableBottomSheetComponent {
|
||||
|
||||
override fun dismiss() {
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val state by stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
val config = remember(state) {
|
||||
TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = ::dismiss,
|
||||
content = state,
|
||||
)
|
||||
}
|
||||
|
||||
TangemModalBottomSheet<AddFundsUM>(
|
||||
config = config,
|
||||
containerColor = TangemTheme.colors2.surface.level2,
|
||||
title = {
|
||||
TangemModalBottomSheetTitle(
|
||||
title = resourceReference(CoreR.string.common_get_token),
|
||||
endIconRes = CoreR.drawable.ic_close_24,
|
||||
onEndClick = ::dismiss,
|
||||
)
|
||||
},
|
||||
content = { contentState ->
|
||||
AddFundsBottomSheetContent(
|
||||
state = contentState,
|
||||
onCloseClick = ::dismiss,
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.tokenaction.TokenActionRow
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.ds.button.SecondaryTangemButton
|
||||
import com.tangem.core.ui.ds.button.TangemButtonShape
|
||||
import com.tangem.core.ui.ds.button.TangemButtonSize
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.LocalHazeState
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM
|
||||
import dev.chrisbanes.haze.rememberHazeState
|
||||
import com.tangem.core.ui.R as CoreR
|
||||
|
||||
@Composable
|
||||
internal fun AddFundsBottomSheetContent(state: AddFundsUM, onCloseClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
|
||||
) {
|
||||
BuyActionRow(state = state)
|
||||
SwapActionRow(state = state)
|
||||
ReceiveActionRow(state = state)
|
||||
|
||||
SpacerH(TangemTheme.dimens2.x2)
|
||||
|
||||
CompositionLocalProvider(LocalHazeState provides rememberHazeState()) {
|
||||
SecondaryTangemButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onCloseClick,
|
||||
text = resourceReference(CoreR.string.common_close),
|
||||
size = TangemButtonSize.X12,
|
||||
shape = TangemButtonShape.Rounded,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(TangemTheme.dimens2.x4)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BuyActionRow(state: AddFundsUM) {
|
||||
val row = (state as? AddFundsUM.Content)?.buy
|
||||
if (state is AddFundsUM.Content && row == null) return
|
||||
ActionRow(
|
||||
iconRes = CoreR.drawable.ic_credit_card_20,
|
||||
title = resourceReference(CoreR.string.common_buy),
|
||||
description = resourceReference(CoreR.string.quick_action_buy_description),
|
||||
row = row,
|
||||
isLoading = state is AddFundsUM.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SwapActionRow(state: AddFundsUM) {
|
||||
val row = (state as? AddFundsUM.Content)?.swap
|
||||
if (state is AddFundsUM.Content && row == null) return
|
||||
ActionRow(
|
||||
iconRes = CoreR.drawable.ic_exchange_mini_24,
|
||||
title = resourceReference(CoreR.string.common_swap),
|
||||
description = resourceReference(CoreR.string.quick_action_swap_description),
|
||||
row = row,
|
||||
isLoading = state is AddFundsUM.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReceiveActionRow(state: AddFundsUM) {
|
||||
val row = (state as? AddFundsUM.Content)?.receive
|
||||
if (state is AddFundsUM.Content && row == null) return
|
||||
ActionRow(
|
||||
iconRes = CoreR.drawable.ic_qrcode_new_24,
|
||||
title = resourceReference(CoreR.string.common_receive),
|
||||
description = resourceReference(CoreR.string.quick_action_receive_description),
|
||||
row = row,
|
||||
isLoading = state is AddFundsUM.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionRow(
|
||||
iconRes: Int,
|
||||
title: TextReference,
|
||||
description: TextReference,
|
||||
row: AddFundsUM.Row?,
|
||||
isLoading: Boolean,
|
||||
) {
|
||||
if (isLoading || row?.isLoading == true) {
|
||||
TokenActionRow(
|
||||
iconRes = iconRes,
|
||||
title = title,
|
||||
description = description,
|
||||
tailContent = { TailLoader() },
|
||||
)
|
||||
} else {
|
||||
TokenActionRow(
|
||||
iconRes = iconRes,
|
||||
title = title,
|
||||
description = description,
|
||||
onClick = row?.onClick,
|
||||
onLongClick = row?.onLongClick,
|
||||
isEnabled = row?.isEnabled == true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TailLoader() {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
color = TangemTheme.colors2.graphic.neutral.tertiary,
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Composable
|
||||
private fun Preview(@PreviewParameter(AddFundsPreviewProvider::class) state: AddFundsUM) {
|
||||
TangemThemePreviewRedesign {
|
||||
CompositionLocalProvider(LocalRedesignEnabled provides true) {
|
||||
AddFundsBottomSheetContent(
|
||||
state = state,
|
||||
onCloseClick = {},
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AddFundsPreviewProvider : PreviewParameterProvider<AddFundsUM> {
|
||||
override val values: Sequence<AddFundsUM> = sequenceOf(
|
||||
AddFundsUM.Loading,
|
||||
AddFundsUM.Content(
|
||||
buy = AddFundsUM.Row(isLoading = false, isEnabled = true, onClick = {}),
|
||||
swap = AddFundsUM.Row(isLoading = false, isEnabled = true, onClick = {}),
|
||||
receive = AddFundsUM.Row(isLoading = false, isEnabled = true, onClick = {}, onLongClick = {}),
|
||||
),
|
||||
AddFundsUM.Content(
|
||||
buy = AddFundsUM.Row(isLoading = false, isEnabled = false, onClick = {}),
|
||||
swap = AddFundsUM.Row(isLoading = false, isEnabled = false, onClick = {}),
|
||||
receive = AddFundsUM.Row(isLoading = false, isEnabled = true, onClick = {}, onLongClick = {}),
|
||||
),
|
||||
AddFundsUM.Content(
|
||||
buy = null,
|
||||
swap = null,
|
||||
receive = AddFundsUM.Row(isLoading = false, isEnabled = true, onClick = {}, onLongClick = {}),
|
||||
),
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -4,16 +4,10 @@ import android.content.res.Configuration
|
|||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -102,8 +96,7 @@ private fun ContentBody(state: TokenDetailsBalanceBlockUM.Content, isBalanceHidd
|
|||
targetState = state.tokenBalanceTypeUM.type,
|
||||
label = "Token balance type",
|
||||
) { currentType ->
|
||||
val tokenBalanceTypeUM = state.tokenBalanceTypeUM
|
||||
when (tokenBalanceTypeUM) {
|
||||
when (val tokenBalanceTypeUM = state.tokenBalanceTypeUM) {
|
||||
is TokenBalanceTypeUM.Multiple -> Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
|
||||
|
|
@ -111,20 +104,20 @@ private fun ContentBody(state: TokenDetailsBalanceBlockUM.Content, isBalanceHidd
|
|||
) {
|
||||
Text(
|
||||
text = currentType.text.resolveReference(),
|
||||
style = TangemTheme.typography2.calloutSemibold15,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
style = TangemTheme.typography2.subheadlineMedium14,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
)
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_sort_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors2.graphic.neutral.secondary,
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x4),
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x5),
|
||||
)
|
||||
}
|
||||
TokenBalanceTypeUM.Single -> Text(
|
||||
text = currentType.text.resolveReference(),
|
||||
style = TangemTheme.typography2.calloutSemibold15,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
style = TangemTheme.typography2.subheadlineMedium14,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -26,7 +22,7 @@ internal fun ZeroBalanceActionsBlock(state: ZeroBalanceActionsUM, modifier: Modi
|
|||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = TangemTheme.dimens2.x10),
|
||||
.padding(bottom = TangemTheme.dimens2.x6),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
|
||||
) {
|
||||
ActionRow(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,347 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.res.R
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.onramp.model.OnrampAvailability
|
||||
import com.tangem.domain.onramp.model.OnrampCountry
|
||||
import com.tangem.domain.onramp.model.OnrampCurrency
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.domain.txhistory.models.TxHistoryStateError
|
||||
import com.tangem.features.tokendetails.TokenDetailsFeatureToggles
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class QuickTopUpBlockFactoryTest {
|
||||
|
||||
private val featureToggles: TokenDetailsFeatureToggles = mockk {
|
||||
every { isQuickTopUpEnabled } returns true
|
||||
}
|
||||
private val factory = QuickTopUpBlockFactory(featureToggles)
|
||||
|
||||
private val zeroBalanceStatus: CryptoCurrencyStatus = mockk {
|
||||
every { value } returns mockk<CryptoCurrencyStatus.Loaded> {
|
||||
every { amount } returns BigDecimal.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
private val nonZeroBalanceStatus: CryptoCurrencyStatus = mockk {
|
||||
every { value } returns mockk<CryptoCurrencyStatus.Loaded> {
|
||||
every { amount } returns BigDecimal.TEN
|
||||
}
|
||||
}
|
||||
|
||||
private val usdCurrency = OnrampCurrency(
|
||||
name = "US Dollar",
|
||||
code = "USD",
|
||||
image = null,
|
||||
precision = 2,
|
||||
unit = "$",
|
||||
)
|
||||
|
||||
private val eurCurrency = OnrampCurrency(
|
||||
name = "Euro",
|
||||
code = "EUR",
|
||||
image = null,
|
||||
precision = 2,
|
||||
unit = "€",
|
||||
)
|
||||
|
||||
private val gbpCurrency = OnrampCurrency(
|
||||
name = "British Pound",
|
||||
code = "GBP",
|
||||
image = null,
|
||||
precision = 2,
|
||||
unit = "£",
|
||||
)
|
||||
|
||||
private val countryMock: OnrampCountry = mockk(relaxed = true)
|
||||
|
||||
private val availableUsd: OnrampAvailability = OnrampAvailability.Available(
|
||||
country = countryMock,
|
||||
currency = usdCurrency,
|
||||
)
|
||||
|
||||
private val notSupported: OnrampAvailability = OnrampAvailability.NotSupported(country = countryMock)
|
||||
|
||||
private val emptyHistory = TxHistoryStateError.EmptyTxHistories.left()
|
||||
private val histWithItems = 5.right()
|
||||
private val histRightZero = 0.right()
|
||||
|
||||
@Test
|
||||
fun `returns null when feature toggle is disabled`() {
|
||||
val disabledToggles: TokenDetailsFeatureToggles = mockk {
|
||||
every { isQuickTopUpEnabled } returns false
|
||||
}
|
||||
val disabledFactory = QuickTopUpBlockFactory(disabledToggles)
|
||||
|
||||
val result = disabledFactory.build(
|
||||
currencyStatus = zeroBalanceStatus,
|
||||
isTxHistoryEmpty = emptyHistory,
|
||||
onrampAvailability = availableUsd.right(),
|
||||
onPresetClick = { _, _ -> },
|
||||
onOtherClick = {},
|
||||
)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns null when balance is non-zero`() {
|
||||
val result = factory.build(
|
||||
currencyStatus = nonZeroBalanceStatus,
|
||||
isTxHistoryEmpty = emptyHistory,
|
||||
onrampAvailability = availableUsd.right(),
|
||||
onPresetClick = { _, _ -> },
|
||||
onOtherClick = {},
|
||||
)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns null when history has transactions`() {
|
||||
val result = factory.build(
|
||||
currencyStatus = zeroBalanceStatus,
|
||||
isTxHistoryEmpty = histWithItems,
|
||||
onrampAvailability = availableUsd.right(),
|
||||
onPresetClick = { _, _ -> },
|
||||
onOtherClick = {},
|
||||
)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns null when onramp is not available`() {
|
||||
val result = factory.build(
|
||||
currencyStatus = zeroBalanceStatus,
|
||||
isTxHistoryEmpty = emptyHistory,
|
||||
onrampAvailability = notSupported.right(),
|
||||
onPresetClick = { _, _ -> },
|
||||
onOtherClick = {},
|
||||
)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns null when currency is not USD or EUR`() {
|
||||
val result = factory.build(
|
||||
currencyStatus = zeroBalanceStatus,
|
||||
isTxHistoryEmpty = emptyHistory,
|
||||
onrampAvailability = OnrampAvailability.Available(
|
||||
country = countryMock,
|
||||
currency = gbpCurrency,
|
||||
).right(),
|
||||
onPresetClick = { _, _ -> },
|
||||
onOtherClick = {},
|
||||
)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns block with USD presets when all conditions met`() {
|
||||
val result = factory.build(
|
||||
currencyStatus = zeroBalanceStatus,
|
||||
isTxHistoryEmpty = emptyHistory,
|
||||
onrampAvailability = availableUsd.right(),
|
||||
onPresetClick = { _, _ -> },
|
||||
onOtherClick = {},
|
||||
)
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
val amounts = result!!.amounts
|
||||
assertThat(amounts.map { it.displayValue }).containsExactly(
|
||||
stringReference("$50"),
|
||||
stringReference("$200"),
|
||||
stringReference("$700"),
|
||||
resourceReference(R.string.quick_top_up_chip_other),
|
||||
).inOrder()
|
||||
assertThat(amounts.last().isOther).isTrue()
|
||||
assertThat(amounts.take(3).all { !it.isOther }).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns block with EUR presets`() {
|
||||
val availableEur = OnrampAvailability.Available(
|
||||
country = countryMock,
|
||||
currency = eurCurrency,
|
||||
)
|
||||
|
||||
val result = factory.build(
|
||||
currencyStatus = zeroBalanceStatus,
|
||||
isTxHistoryEmpty = emptyHistory,
|
||||
onrampAvailability = availableEur.right(),
|
||||
onPresetClick = { _, _ -> },
|
||||
onOtherClick = {},
|
||||
)
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
val amounts = result!!.amounts
|
||||
assertThat(amounts.map { it.displayValue }).containsExactly(
|
||||
stringReference("€50"),
|
||||
stringReference("€200"),
|
||||
stringReference("€650"),
|
||||
resourceReference(R.string.quick_top_up_chip_other),
|
||||
).inOrder()
|
||||
assertThat(amounts.last().isOther).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns block when history count is right zero (boundary case)`() {
|
||||
val result = factory.build(
|
||||
currencyStatus = zeroBalanceStatus,
|
||||
isTxHistoryEmpty = histRightZero,
|
||||
onrampAvailability = availableUsd.right(),
|
||||
onPresetClick = { _, _ -> },
|
||||
onOtherClick = {},
|
||||
)
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns block when ConfirmResidency and country supports onramp with USD`() {
|
||||
val usdCountry = OnrampCountry(
|
||||
id = "us",
|
||||
name = "United States",
|
||||
code = "US",
|
||||
image = "",
|
||||
alpha3 = "USA",
|
||||
continent = "America",
|
||||
defaultCurrency = usdCurrency,
|
||||
onrampAvailable = true,
|
||||
)
|
||||
val confirmResidency = OnrampAvailability.ConfirmResidency(country = usdCountry)
|
||||
|
||||
val result = factory.build(
|
||||
currencyStatus = zeroBalanceStatus,
|
||||
isTxHistoryEmpty = emptyHistory,
|
||||
onrampAvailability = confirmResidency.right(),
|
||||
onPresetClick = { _, _ -> },
|
||||
onOtherClick = {},
|
||||
)
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
val amounts = result!!.amounts
|
||||
assertThat(amounts.map { it.displayValue }).containsExactly(
|
||||
stringReference("$50"),
|
||||
stringReference("$200"),
|
||||
stringReference("$700"),
|
||||
resourceReference(R.string.quick_top_up_chip_other),
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns null when onramp availability is error`() {
|
||||
val result = factory.build(
|
||||
currencyStatus = zeroBalanceStatus,
|
||||
isTxHistoryEmpty = emptyHistory,
|
||||
onrampAvailability = OnrampError.DataError(code = "error", description = null).left(),
|
||||
onPresetClick = { _, _ -> },
|
||||
onOtherClick = {},
|
||||
)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns null when balance is loading (amount is null)`() {
|
||||
val loadingStatus: CryptoCurrencyStatus = mockk {
|
||||
every { value } returns CryptoCurrencyStatus.Loading
|
||||
}
|
||||
|
||||
val result = factory.build(
|
||||
currencyStatus = loadingStatus,
|
||||
isTxHistoryEmpty = emptyHistory,
|
||||
onrampAvailability = availableUsd.right(),
|
||||
onPresetClick = { _, _ -> },
|
||||
onOtherClick = {},
|
||||
)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns null when tx history is not implemented`() {
|
||||
val result = factory.build(
|
||||
currencyStatus = zeroBalanceStatus,
|
||||
isTxHistoryEmpty = TxHistoryStateError.TxHistoryNotImplemented.left(),
|
||||
onrampAvailability = availableUsd.right(),
|
||||
onPresetClick = { _, _ -> },
|
||||
onOtherClick = {},
|
||||
)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns null when tx history fetch fails with data error`() {
|
||||
val result = factory.build(
|
||||
currencyStatus = zeroBalanceStatus,
|
||||
isTxHistoryEmpty = TxHistoryStateError.DataError(RuntimeException("network error")).left(),
|
||||
onrampAvailability = availableUsd.right(),
|
||||
onPresetClick = { _, _ -> },
|
||||
onOtherClick = {},
|
||||
)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns null when ConfirmResidency with non-USD or EUR default currency`() {
|
||||
val gbpCountry = OnrampCountry(
|
||||
id = "gb",
|
||||
name = "United Kingdom",
|
||||
code = "GB",
|
||||
image = "",
|
||||
alpha3 = "GBR",
|
||||
continent = "Europe",
|
||||
defaultCurrency = gbpCurrency,
|
||||
onrampAvailable = true,
|
||||
)
|
||||
|
||||
val result = factory.build(
|
||||
currencyStatus = zeroBalanceStatus,
|
||||
isTxHistoryEmpty = emptyHistory,
|
||||
onrampAvailability = OnrampAvailability.ConfirmResidency(country = gbpCountry).right(),
|
||||
onPresetClick = { _, _ -> },
|
||||
onOtherClick = {},
|
||||
)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns null when ConfirmResidency but country does not support onramp`() {
|
||||
val restrictedCountry = OnrampCountry(
|
||||
id = "kp",
|
||||
name = "North Korea",
|
||||
code = "KP",
|
||||
image = "",
|
||||
alpha3 = "PRK",
|
||||
continent = "Asia",
|
||||
defaultCurrency = usdCurrency,
|
||||
onrampAvailable = false,
|
||||
)
|
||||
val confirmResidency = OnrampAvailability.ConfirmResidency(country = restrictedCountry)
|
||||
|
||||
val result = factory.build(
|
||||
currencyStatus = zeroBalanceStatus,
|
||||
isTxHistoryEmpty = emptyHistory,
|
||||
onrampAvailability = confirmResidency.right(),
|
||||
onPresetClick = { _, _ -> },
|
||||
onOtherClick = {},
|
||||
)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue