Updated on 2026-08-14
This commit is contained in:
parent
8966e28e29
commit
799a38655e
9 changed files with 733 additions and 67 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue