Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-10 11:10:57 +03:00
parent 8966e28e29
commit 799a38655e
9 changed files with 733 additions and 67 deletions

View file

@ -1,42 +1,65 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.model
import com.tangem.common.core.TangemSdkError
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.res.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.common.ui.amountScreen.utils.getFiatString
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.dynamicaddresses.CreateConsolidationTransactionUseCase
import com.tangem.domain.dynamicaddresses.DisableDynamicAddressesUseCase
import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesError
import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesUseCase
import com.tangem.domain.dynamicaddresses.IsXpubDerivedUseCase
import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.utils.Provider
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.dynamicaddresses.DynamicAddressesBottomSheetConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.logging.TangemLogger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@Suppress("LongParameterList")
internal class DynamicAddressesDelegate(
internal class DynamicAddressesDelegate @AssistedInject constructor(
private val enableDynamicAddressesUseCase: EnableDynamicAddressesUseCase,
private val disableDynamicAddressesUseCase: DisableDynamicAddressesUseCase,
private val createConsolidationTransactionUseCase: CreateConsolidationTransactionUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val sendTransactionUseCase: SendTransactionUseCase,
private val isXpubDerivedUseCase: IsXpubDerivedUseCase,
private val dynamicAddressesRepository: DynamicAddressesRepository,
private val getExtendedPublicKeyUseCase: GetExtendedPublicKeyForCurrencyUseCase,
private val uiMessageSender: UiMessageSender,
private val userWalletId: UserWalletId,
private val network: Network,
private val coroutineScope: CoroutineScope,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
private val dispatchers: CoroutineDispatcherProvider,
private val showBottomSheet: () -> Unit,
private val dismissBottomSheet: () -> Unit,
private val onDynamicAddressesEnabled: () -> Unit,
@Assisted private val userWallet: UserWallet,
@Assisted private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
@Assisted private val appCurrencyProvider: Provider<AppCurrency>,
@Assisted private val coroutineScope: CoroutineScope,
@Assisted("showBottomSheet") private val showBottomSheet: () -> Unit,
@Assisted("dismissBottomSheet") private val dismissBottomSheet: () -> Unit,
@Assisted("onDynamicAddressesStateChanged") private val onDynamicAddressesStateChanged: () -> Unit,
) {
private val userWalletId get() = userWallet.walletId
private val _bottomSheetConfig = MutableStateFlow<DynamicAddressesBottomSheetConfig>(
DynamicAddressesBottomSheetConfig.Enable(
isCardScanRequired = false,
@ -45,27 +68,45 @@ internal class DynamicAddressesDelegate(
)
val bottomSheetConfig: StateFlow<DynamicAddressesBottomSheetConfig> = _bottomSheetConfig.asStateFlow()
fun onDynamicAddressesClick() {
coroutineScope.launch(dispatchers.main) {
val hasConflicts = dynamicAddressesRepository.hasConflictingCustomTokens(userWalletId, network)
if (hasConflicts) {
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.Unavailable(
onGotItClick = dismissBottomSheet,
)
showBottomSheet()
return@launch
}
// region Entry point
val isCardScanRequired = !isXpubAlreadyDerived()
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.Enable(
isCardScanRequired = isCardScanRequired,
onEnableClick = ::onEnableClick,
)
showBottomSheet()
fun onDynamicAddressesClick() {
val network = cryptoCurrencyStatusProvider()?.currency?.network ?: return
coroutineScope.launch(dispatchers.main) {
val status = dynamicAddressesRepository.getStatus(userWalletId, network).first()
when (status) {
DynamicAddressesStatus.ENABLED,
DynamicAddressesStatus.ENABLED_REQUIRES_SETUP,
-> onDisableFlow(network)
DynamicAddressesStatus.DISABLED -> onEnableFlow(network)
}
}
}
// endregion
// region Enable flow
private suspend fun onEnableFlow(network: Network) {
val hasConflicts = dynamicAddressesRepository.hasConflictingCustomTokens(userWalletId, network)
if (hasConflicts) {
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ConflictingCustomTokens(
onDismissClick = dismissBottomSheet,
)
showBottomSheet()
return
}
val isCardScanRequired = !isXpubAlreadyDerived(network)
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.Enable(
isCardScanRequired = isCardScanRequired,
onEnableClick = ::onEnableClick,
)
showBottomSheet()
}
private fun onEnableClick() {
val network = cryptoCurrencyStatusProvider()?.currency?.network ?: return
coroutineScope.launch(dispatchers.main) {
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.Enable(
isCardScanRequired = false,
@ -80,7 +121,7 @@ internal class DynamicAddressesDelegate(
} else {
TangemLogger.e("Failed to get XPUB: ${error.message}")
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable(
onGotItClick = dismissBottomSheet,
onDismissClick = dismissBottomSheet,
)
}
return@launch
@ -92,21 +133,21 @@ internal class DynamicAddressesDelegate(
ifLeft = { error ->
when (error) {
is EnableDynamicAddressesError.ConflictingCustomTokens -> {
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.Unavailable(
onGotItClick = dismissBottomSheet,
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ConflictingCustomTokens(
onDismissClick = dismissBottomSheet,
)
}
is EnableDynamicAddressesError.ServiceError -> {
TangemLogger.e("Failed to enable DA: ${error.cause.message}")
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable(
onGotItClick = dismissBottomSheet,
onDismissClick = dismissBottomSheet,
)
}
}
},
ifRight = {
dismissBottomSheet()
onDynamicAddressesEnabled()
onDynamicAddressesStateChanged()
uiMessageSender.send(
SnackbarMessage(message = resourceReference(R.string.dynamic_addresses_enabled_toast_title)),
)
@ -115,11 +156,208 @@ internal class DynamicAddressesDelegate(
}
}
private suspend fun isXpubAlreadyDerived(): Boolean {
// endregion
// region Disable flow
private fun onDisableFlow(network: Network) {
coroutineScope.launch(dispatchers.main) {
disableDynamicAddressesUseCase(userWalletId, network).fold(
ifLeft = { error ->
TangemLogger.e("Failed to check disable: ${error.message}")
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable(
onDismissClick = dismissBottomSheet,
)
showBottomSheet()
},
ifRight = { isConsolidationRequired ->
if (!isConsolidationRequired) {
showSimpleDisableSheet()
} else {
showDisableSheetAndLoadFee()
}
},
)
}
}
private fun showSimpleDisableSheet() {
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.DisableWithoutConsolidation(
onDisableClick = ::onSimpleDisableClick,
onReadMoreClick = ::onReadMoreClick,
)
showBottomSheet()
}
private fun onSimpleDisableClick() {
val network = cryptoCurrencyStatusProvider()?.currency?.network ?: return
coroutineScope.launch(dispatchers.main) {
runSuspendCatching { dynamicAddressesRepository.disable(userWalletId, network) }
.onSuccess {
dismissBottomSheet()
onDynamicAddressesStateChanged()
uiMessageSender.send(
SnackbarMessage(message = resourceReference(R.string.dynamic_addresses_disabled_popup_title)),
)
}
.onFailure { e ->
TangemLogger.e("Failed to disable DA: ${e.message}")
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable(
onDismissClick = dismissBottomSheet,
)
}
}
}
private fun showDisableSheetAndLoadFee() {
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.DisableWithConsolidation(
feeState = DynamicAddressesBottomSheetConfig.DisableFeeState.Loading,
onDisableClick = ::onDisableClick,
onRefreshFee = ::loadDisableFee,
onReadMoreClick = ::onReadMoreClick,
)
showBottomSheet()
loadDisableFee()
}
private fun loadDisableFee() {
coroutineScope.launch(dispatchers.main) {
_bottomSheetConfig.value = disableWithConsolidationConfig().copy(
feeState = DynamicAddressesBottomSheetConfig.DisableFeeState.Loading,
)
val status = cryptoCurrencyStatusProvider()
val currency = status?.currency
val balance = status?.value?.amount
val address = status?.value?.networkAddress?.defaultAddress?.value
if (currency == null || balance == null || address == null) {
_bottomSheetConfig.value = disableWithConsolidationConfig().copy(
feeState = DynamicAddressesBottomSheetConfig.DisableFeeState.Error,
)
return@launch
}
getFeeUseCase(
amount = balance,
destination = address,
userWallet = userWallet,
cryptoCurrency = currency,
).fold(
ifLeft = {
_bottomSheetConfig.value = disableWithConsolidationConfig().copy(
feeState = DynamicAddressesBottomSheetConfig.DisableFeeState.Error,
)
},
ifRight = { txFee ->
val fee = txFee.normal
val fiatFormatted = getFiatString(
value = fee.amount.value,
rate = status.value.fiatRate,
appCurrency = appCurrencyProvider(),
approximate = true,
)
_bottomSheetConfig.value = disableWithConsolidationConfig().copy(
feeState = DynamicAddressesBottomSheetConfig.DisableFeeState.Content(
feeSymbol = currency.symbol,
fiatFormatted = fiatFormatted,
),
)
},
)
}
}
private fun disableWithConsolidationConfig(): DynamicAddressesBottomSheetConfig.DisableWithConsolidation {
return _bottomSheetConfig.value as? DynamicAddressesBottomSheetConfig.DisableWithConsolidation
?: DynamicAddressesBottomSheetConfig.DisableWithConsolidation(
onDisableClick = ::onDisableClick,
onRefreshFee = ::loadDisableFee,
onReadMoreClick = ::onReadMoreClick,
)
}
// TODO: Replace with actual DA documentation URL
private fun onReadMoreClick() {
// Stub: open documentation about Dynamic Addresses consolidation
}
private fun onDisableClick() {
val network = cryptoCurrencyStatusProvider()?.currency?.network ?: return
coroutineScope.launch(dispatchers.main) {
_bottomSheetConfig.value = disableWithConsolidationConfig().copy(
isSending = true,
)
val txData = createConsolidationTransactionUseCase(userWalletId, network).fold(
ifLeft = { error ->
TangemLogger.e("Failed to create consolidation tx: ${error.message}")
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable(
onDismissClick = dismissBottomSheet,
)
return@launch
},
ifRight = { it },
)
sendTransactionUseCase(
txData = txData,
userWallet = userWallet,
network = network,
).fold(
ifLeft = { error ->
if (error is SendTransactionError.UserCancelledError) {
dismissBottomSheet()
} else {
TangemLogger.e("Failed to send consolidation tx: $error")
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable(
onDismissClick = dismissBottomSheet,
)
}
},
ifRight = {
try {
dynamicAddressesRepository.disable(userWalletId, network)
} catch (e: Exception) {
TangemLogger.e("Failed to disable DA after consolidation: ${e.message}")
}
dismissBottomSheet()
onDynamicAddressesStateChanged()
uiMessageSender.send(
SnackbarMessage(
message = resourceReference(R.string.dynamic_addresses_disabled_popup_title),
),
)
},
)
}
}
// endregion
// region Common
private suspend fun isXpubAlreadyDerived(network: Network): Boolean {
return isXpubDerivedUseCase(userWalletId, network)
}
private fun isUserCancellation(error: Throwable): Boolean {
return error is TangemSdkError.UserCancelled || error.cause is TangemSdkError.UserCancelled
}
// endregion
@AssistedFactory
interface Factory {
@Suppress("LongParameterList")
fun create(
userWallet: UserWallet,
cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
appCurrencyProvider: Provider<AppCurrency>,
coroutineScope: CoroutineScope,
@Assisted("showBottomSheet") showBottomSheet: () -> Unit,
@Assisted("dismissBottomSheet") dismissBottomSheet: () -> Unit,
@Assisted("onDynamicAddressesStateChanged") onDynamicAddressesStateChanged: () -> Unit,
): DynamicAddressesDelegate
}
}

View file

@ -11,10 +11,7 @@ import com.tangem.blockchain.common.address.AddressType
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
import com.tangem.domain.dynamicaddresses.DynamicAddressesSupportedBlockchains
import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesUseCase
import com.tangem.domain.dynamicaddresses.IsXpubDerivedUseCase
import com.tangem.domain.dynamicaddresses.IsXpubSupportedUseCase
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.bottomsheet.receive.AddressModel
@ -170,11 +167,9 @@ internal class TokenDetailsModel @Inject constructor(
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
private val yieldSupplyGetRewardsBalanceUseCase: YieldSupplyGetRewardsBalanceUseCase,
private val signCloreMessageUseCase: SignCloreMessageUseCase,
private val enableDynamicAddressesUseCase: EnableDynamicAddressesUseCase,
private val isXpubSupportedUseCase: IsXpubSupportedUseCase,
private val isXpubDerivedUseCase: IsXpubDerivedUseCase,
private val dynamicAddressesRepository: DynamicAddressesRepository,
private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
private val dynamicAddressesDelegateFactory: DynamicAddressesDelegate.Factory,
private val dialogFactory: TokenDetailsDialogFactory,
private val userWalletsListRepository: UserWalletsListRepository,
private val singleAccountListSupplier: SingleAccountListSupplier,
@ -246,21 +241,16 @@ internal class TokenDetailsModel @Inject constructor(
// region Dynamic Addresses
val dynamicAddressesDelegate by lazy(mode = LazyThreadSafetyMode.NONE) {
DynamicAddressesDelegate(
enableDynamicAddressesUseCase = enableDynamicAddressesUseCase,
isXpubDerivedUseCase = isXpubDerivedUseCase,
dynamicAddressesRepository = dynamicAddressesRepository,
getExtendedPublicKeyUseCase = getExtendedPublicKeyForCurrencyUseCase,
uiMessageSender = uiMessageSender,
userWalletId = userWalletId,
network = cryptoCurrency.network,
dynamicAddressesDelegateFactory.create(
userWallet = userWallet,
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
appCurrencyProvider = Provider { selectedAppCurrencyFlow.value },
coroutineScope = modelScope,
dispatchers = dispatchers,
showBottomSheet = {
bottomSheetNavigation.activate(TokenDetailsBottomSheetConfig.DynamicAddresses)
},
dismissBottomSheet = bottomSheetNavigation::dismiss,
onDynamicAddressesEnabled = ::onDynamicAddressesEnabled,
onDynamicAddressesStateChanged = ::onDynamicAddressesStateChanged,
)
}
// endregion Dynamic Addresses
@ -707,7 +697,7 @@ internal class TokenDetailsModel @Inject constructor(
override fun onDynamicAddressesClick() = dynamicAddressesDelegate.onDynamicAddressesClick()
private fun onDynamicAddressesEnabled() {
private fun onDynamicAddressesStateChanged() {
updateTopBarMenu()
modelScope.launch(dispatchers.main) {
cryptoCurrencyBalanceFetcher.invokeAndAwait(userWalletId = userWalletId, currency = cryptoCurrency)

View file

@ -4,12 +4,14 @@ import androidx.compose.runtime.Composable
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.res.TangemTheme
import com.tangem.core.ui.R as CoreR
@Composable
internal fun DynamicAddressesBottomSheet(config: TangemBottomSheetConfig) {
TangemModalBottomSheet<DynamicAddressesBottomSheetConfig>(
config = config,
containerColor = TangemTheme.colors.background.tertiary,
title = {
TangemModalBottomSheetTitle(
endIconRes = CoreR.drawable.ic_close_24,
@ -19,10 +21,18 @@ internal fun DynamicAddressesBottomSheet(config: TangemBottomSheetConfig) {
) { content ->
when (content) {
is DynamicAddressesBottomSheetConfig.Enable -> DynamicAddressesEnableContent(content = content)
is DynamicAddressesBottomSheetConfig.Unavailable -> DynamicAddressesUnavailableContent(content = content)
is DynamicAddressesBottomSheetConfig.ServiceUnavailable -> DynamicAddressesServiceUnavailableContent(
content = content,
)
is DynamicAddressesBottomSheetConfig.DisableWithoutConsolidation -> {
DynamicAddressesDisableWithoutConsolidationContent(content = content)
}
is DynamicAddressesBottomSheetConfig.DisableWithConsolidation -> {
DynamicAddressesDisableWithConsolidationContent(content = content)
}
is DynamicAddressesBottomSheetConfig.ConflictingCustomTokens -> {
DynamicAddressesConflictingCustomTokensContent(content = content)
}
is DynamicAddressesBottomSheetConfig.ServiceUnavailable -> {
DynamicAddressesServiceUnavailableContent(content = content)
}
}
}
}

View file

@ -12,11 +12,33 @@ internal sealed class DynamicAddressesBottomSheetConfig : TangemBottomSheetConfi
val onEnableClick: () -> Unit,
) : DynamicAddressesBottomSheetConfig()
data class Unavailable(
val onGotItClick: () -> Unit,
data class DisableWithoutConsolidation(
val onDisableClick: () -> Unit,
val onReadMoreClick: () -> Unit,
) : DynamicAddressesBottomSheetConfig()
data class DisableWithConsolidation(
val feeState: DisableFeeState = DisableFeeState.Loading,
val isSending: Boolean = false,
val onDisableClick: () -> Unit,
val onRefreshFee: () -> Unit,
val onReadMoreClick: () -> Unit,
) : DynamicAddressesBottomSheetConfig()
sealed interface DisableFeeState {
data object Loading : DisableFeeState
data class Content(
val feeSymbol: String,
val fiatFormatted: String,
) : DisableFeeState
data object Error : DisableFeeState
}
data class ConflictingCustomTokens(
val onDismissClick: () -> Unit,
) : DynamicAddressesBottomSheetConfig()
data class ServiceUnavailable(
val onGotItClick: () -> Unit,
val onDismissClick: () -> Unit,
) : DynamicAddressesBottomSheetConfig()
}

View file

@ -1,5 +1,7 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.dynamicaddresses
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -14,12 +16,28 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.audits.AuditLabel
import com.tangem.core.ui.components.audits.AuditLabelUM
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.res.R
import com.tangem.core.ui.R as CoreR
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.dynamicaddresses.DynamicAddressesBottomSheetConfig.DisableFeeState
@Composable
internal fun DynamicAddressesEnableContent(content: DynamicAddressesBottomSheetConfig.Enable) {
@ -72,13 +90,13 @@ internal fun DynamicAddressesEnableContent(content: DynamicAddressesBottomSheetC
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing24))
PrimaryButton(
PrimaryButtonIconEnd(
text = stringResourceSafe(id = R.string.dynamic_addresses_enter_main_button_title),
iconResId = if (content.isCardScanRequired) CoreR.drawable.ic_tangem_24 else null,
onClick = content.onEnableClick,
modifier = Modifier.fillMaxWidth(),
showProgress = content.isLoading,
enabled = !content.isLoading,
// TODO add card icon when isCardScanRequired
)
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing16))
@ -86,12 +104,205 @@ internal fun DynamicAddressesEnableContent(content: DynamicAddressesBottomSheetC
}
@Composable
internal fun DynamicAddressesUnavailableContent(content: DynamicAddressesBottomSheetConfig.Unavailable) {
internal fun DynamicAddressesDisableWithoutConsolidationContent(
content: DynamicAddressesBottomSheetConfig.DisableWithoutConsolidation,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
horizontalAlignment = Alignment.CenterHorizontally,
) {
DisableHeader()
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing24))
PrimaryButtonIconEnd(
text = stringResourceSafe(id = R.string.dynamic_addresses_disable_main_button_title),
iconResId = null,
onClick = content.onDisableClick,
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing16))
}
}
@Composable
internal fun DynamicAddressesDisableWithConsolidationContent(
content: DynamicAddressesBottomSheetConfig.DisableWithConsolidation,
) {
val isConfirmEnabled = content.feeState is DisableFeeState.Content && !content.isSending
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
horizontalAlignment = Alignment.CenterHorizontally,
) {
DisableHeader()
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing16))
DisableFeeBlock(
feeState = content.feeState,
onReadMoreClick = content.onReadMoreClick,
)
if (content.feeState is DisableFeeState.Error) {
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing12))
Notification(
config = NotificationConfig(
title = resourceReference(R.string.send_fee_unreachable_error_title),
subtitle = resourceReference(R.string.send_fee_unreachable_error_text),
iconResId = CoreR.drawable.ic_alert_24,
iconTint = NotificationConfig.IconTint.Warning,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.warning_button_refresh),
onClick = content.onRefreshFee,
),
),
containerColor = TangemTheme.colors.background.action,
)
}
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing24))
PrimaryButtonIconEnd(
text = stringResourceSafe(id = R.string.dynamic_addresses_disable_main_button_title),
iconResId = CoreR.drawable.ic_tangem_24,
onClick = content.onDisableClick,
modifier = Modifier.fillMaxWidth(),
showProgress = content.isSending,
enabled = isConfirmEnabled,
)
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing16))
}
}
@Composable
private fun DisableHeader() {
Icon(
painter = painterResource(id = CoreR.drawable.ic_dynamic_addresses_bottomsheet_enable_unavailable),
contentDescription = null,
modifier = Modifier.size(TangemTheme.dimens.size44),
tint = TangemTheme.colors.icon.attention,
)
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing12))
Text(
text = stringResourceSafe(id = R.string.dynamic_addresses_disable_title),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing8))
Text(
text = stringResourceSafe(id = R.string.dynamic_addresses_disable_description),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
}
@Composable
private fun DisableFeeBlock(feeState: DisableFeeState, onReadMoreClick: () -> Unit) {
Column(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(TangemTheme.dimens.spacing12),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResourceSafe(id = R.string.common_network_fee_title),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
when (feeState) {
is DisableFeeState.Loading -> TextShimmer(
style = TangemTheme.typography.body2,
modifier = Modifier.size(
width = TangemTheme.dimens.size80,
height = TangemTheme.dimens.spacing16,
),
)
is DisableFeeState.Content -> Row(
verticalAlignment = Alignment.CenterVertically,
) {
AuditLabel(
state = AuditLabelUM(
text = stringReference(feeState.feeSymbol),
type = AuditLabelUM.Type.General,
),
)
Text(
text = feeState.fiatFormatted,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
)
}
is DisableFeeState.Error -> Text(
text = "\u2014",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
}
}
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing8))
DisableFeeDescription(onReadMoreClick = onReadMoreClick)
}
}
@Composable
private fun DisableFeeDescription(onReadMoreClick: () -> Unit) {
val readMoreText = stringResourceSafe(id = R.string.common_read_more)
val fullText = stringResourceSafe(id = R.string.dynamic_addresses_disable_fee_description)
val annotatedString = buildAnnotatedString {
withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) {
append(fullText)
append(" ")
}
withLink(
link = LinkAnnotation.Clickable(
tag = "read_more",
linkInteractionListener = { onReadMoreClick() },
),
) {
withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) {
append(readMoreText)
}
}
}
Text(
text = annotatedString,
style = TangemTheme.typography.caption2,
)
}
@Composable
internal fun DynamicAddressesConflictingCustomTokensContent(
content: DynamicAddressesBottomSheetConfig.ConflictingCustomTokens,
) {
ErrorContent(
titleRes = R.string.dynamic_addresses_error_has_custom_token_title,
descriptionRes = R.string.dynamic_addresses_error_has_custom_token_description,
buttonTextRes = R.string.common_got_it,
onButtonClick = content.onGotItClick,
onButtonClick = content.onDismissClick,
)
}
@ -101,7 +312,7 @@ internal fun DynamicAddressesServiceUnavailableContent(content: DynamicAddresses
titleRes = R.string.dynamic_addresses_error_service_unavailable_title,
descriptionRes = R.string.dynamic_addresses_error_service_unavailable_description,
buttonTextRes = R.string.common_got_it,
onButtonClick = content.onGotItClick,
onButtonClick = content.onDismissClick,
)
}
@ -117,7 +328,7 @@ private fun ErrorContent(titleRes: Int, descriptionRes: Int, buttonTextRes: Int,
painter = painterResource(id = CoreR.drawable.ic_dynamic_addresses_bottomsheet_enable_unavailable),
contentDescription = null,
modifier = Modifier.size(TangemTheme.dimens.size44),
tint = TangemTheme.colors.icon.warning,
tint = TangemTheme.colors.icon.attention,
)
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing12))
@ -176,4 +387,143 @@ private fun FeatureItem(iconRes: Int, title: String, description: String) {
)
}
}
}
}
// region Previews
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_Enable() {
TangemThemePreview {
DynamicAddressesEnableContent(
content = DynamicAddressesBottomSheetConfig.Enable(
isCardScanRequired = false,
onEnableClick = {},
),
)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_EnableWithCardScan() {
TangemThemePreview {
DynamicAddressesEnableContent(
content = DynamicAddressesBottomSheetConfig.Enable(
isCardScanRequired = true,
onEnableClick = {},
),
)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_DisableWithoutConsolidation() {
TangemThemePreview {
DynamicAddressesDisableWithoutConsolidationContent(
content = DynamicAddressesBottomSheetConfig.DisableWithoutConsolidation(
onDisableClick = {},
onReadMoreClick = {},
),
)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_DisableFeeLoading() {
TangemThemePreview {
DynamicAddressesDisableWithConsolidationContent(
content = DynamicAddressesBottomSheetConfig.DisableWithConsolidation(
feeState = DisableFeeState.Loading,
onDisableClick = {},
onRefreshFee = {},
onReadMoreClick = {},
),
)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_DisableFeeLoaded() {
TangemThemePreview {
DynamicAddressesDisableWithConsolidationContent(
content = DynamicAddressesBottomSheetConfig.DisableWithConsolidation(
feeState = DisableFeeState.Content(
feeSymbol = "BTC",
fiatFormatted = "~$0.12",
),
onDisableClick = {},
onRefreshFee = {},
onReadMoreClick = {},
),
)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_DisableFeeError() {
TangemThemePreview {
DynamicAddressesDisableWithConsolidationContent(
content = DynamicAddressesBottomSheetConfig.DisableWithConsolidation(
feeState = DisableFeeState.Error,
onDisableClick = {},
onRefreshFee = {},
onReadMoreClick = {},
),
)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_DisableSending() {
TangemThemePreview {
DynamicAddressesDisableWithConsolidationContent(
content = DynamicAddressesBottomSheetConfig.DisableWithConsolidation(
feeState = DisableFeeState.Content(
feeSymbol = "BTC",
fiatFormatted = "~$0.12",
),
isSending = true,
onDisableClick = {},
onRefreshFee = {},
onReadMoreClick = {},
),
)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ConflictingCustomTokens() {
TangemThemePreview {
DynamicAddressesConflictingCustomTokensContent(
content = DynamicAddressesBottomSheetConfig.ConflictingCustomTokens(onDismissClick = {}),
)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ServiceUnavailable() {
TangemThemePreview {
DynamicAddressesServiceUnavailableContent(
content = DynamicAddressesBottomSheetConfig.ServiceUnavailable(onDismissClick = {}),
)
}
}
// endregion