Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-08 14:09:57 +03:00
parent 52a607c1f5
commit 9944036c4d
29 changed files with 874 additions and 60 deletions

View file

@ -24,6 +24,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetails
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreenLegacy
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.ChooseAddressBottomSheetComponent
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.CloreMigrationBottomSheetComponent
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.DynamicAddressesBottomSheetComponent
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.tokenreceive.TokenReceiveComponent
@ -148,6 +149,10 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
cloreMigrationModel = model.cloreMigrationModel,
onDismiss = model.bottomSheetNavigation::dismiss,
)
is TokenDetailsBottomSheetConfig.DynamicAddresses -> DynamicAddressesBottomSheetComponent(
dynamicAddressesDelegate = model.dynamicAddressesDelegate,
onDismiss = model.bottomSheetNavigation::dismiss,
)
}
@AssistedFactory

View file

@ -0,0 +1,125 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.model
import com.tangem.common.core.TangemSdkError
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.domain.dynamicaddresses.EnableDynamicAddressesError
import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesUseCase
import com.tangem.domain.dynamicaddresses.IsXpubDerivedUseCase
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
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.logging.TangemLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
@Suppress("LongParameterList")
internal class DynamicAddressesDelegate(
private val enableDynamicAddressesUseCase: EnableDynamicAddressesUseCase,
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,
private val dispatchers: CoroutineDispatcherProvider,
private val showBottomSheet: () -> Unit,
private val dismissBottomSheet: () -> Unit,
private val onDynamicAddressesEnabled: () -> Unit,
) {
private val _bottomSheetConfig = MutableStateFlow<DynamicAddressesBottomSheetConfig>(
DynamicAddressesBottomSheetConfig.Enable(
isCardScanRequired = false,
onEnableClick = {},
),
)
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
}
val isCardScanRequired = !isXpubAlreadyDerived()
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.Enable(
isCardScanRequired = isCardScanRequired,
onEnableClick = ::onEnableClick,
)
showBottomSheet()
}
}
private fun onEnableClick() {
coroutineScope.launch(dispatchers.main) {
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.Enable(
isCardScanRequired = false,
isLoading = true,
onEnableClick = {},
)
val xpub = getExtendedPublicKeyUseCase(userWalletId, network).fold(
ifLeft = { error ->
if (isUserCancellation(error)) {
dismissBottomSheet()
} else {
TangemLogger.e("Failed to get XPUB: ${error.message}")
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable(
onGotItClick = dismissBottomSheet,
)
}
return@launch
},
ifRight = { it },
)
enableDynamicAddressesUseCase(userWalletId, network, xpub).fold(
ifLeft = { error ->
when (error) {
is EnableDynamicAddressesError.ConflictingCustomTokens -> {
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.Unavailable(
onGotItClick = dismissBottomSheet,
)
}
is EnableDynamicAddressesError.ServiceError -> {
TangemLogger.e("Failed to enable DA: ${error.cause.message}")
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable(
onGotItClick = dismissBottomSheet,
)
}
}
},
ifRight = {
dismissBottomSheet()
onDynamicAddressesEnabled()
uiMessageSender.send(
SnackbarMessage(message = resourceReference(R.string.dynamic_addresses_enabled_toast_title)),
)
},
)
}
}
private suspend fun isXpubAlreadyDerived(): Boolean {
return isXpubDerivedUseCase(userWalletId, network)
}
private fun isUserCancellation(error: Throwable): Boolean {
return error is TangemSdkError.UserCancelled || error.cause is TangemSdkError.UserCancelled
}
}

View file

@ -48,6 +48,8 @@ interface TokenDetailsClickIntents {
fun onGenerateExtendedKey()
fun onDynamicAddressesClick()
fun onCopyAddress(): TextReference?
fun onAssociateClick()
@ -125,6 +127,8 @@ internal class EmptyTokenDetailsClickIntents : TokenDetailsClickIntents {
override fun onGenerateExtendedKey() { /* no op */ }
override fun onDynamicAddressesClick() { /* no op */ }
override fun onSellClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ }
override fun onSwapClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ }

View file

@ -2,13 +2,19 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import arrow.core.merge
import arrow.core.right
import com.tangem.utils.logging.TangemLogger
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
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
@ -16,9 +22,7 @@ import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.di.ModelScoped
@ -53,6 +57,7 @@ import com.tangem.domain.models.TokenReceiveNotification
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -154,7 +159,6 @@ internal class TokenDetailsModel @Inject constructor(
private val appRouter: AppRouter,
private val router: InnerTokenDetailsRouter,
private val tokenDetailsDeepLinkActionListener: TokenDetailsDeepLinkActionListener,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
private val receiveAddressesFactory: ReceiveAddressesFactory,
private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase,
private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase,
@ -163,6 +167,11 @@ 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,
) : Model(),
TokenDetailsClickIntents,
ExpressTransactionsClickIntents,
@ -226,6 +235,27 @@ internal class TokenDetailsModel @Inject constructor(
}
// endregion
// 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,
coroutineScope = modelScope,
dispatchers = dispatchers,
showBottomSheet = {
bottomSheetNavigation.activate(TokenDetailsBottomSheetConfig.DynamicAddresses)
},
dismissBottomSheet = bottomSheetNavigation::dismiss,
onDynamicAddressesEnabled = ::onDynamicAddressesEnabled,
)
}
// endregion Dynamic Addresses
private val expressStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
tokenDetailsExpressStatusFactory.create(
clickIntents = this,
@ -485,39 +515,45 @@ internal class TokenDetailsModel @Inject constructor(
).getOrElse { false }
val isSupported = isXPUBSupported()
val isDynamicAddressesAvailable = isSupported && isDynamicAddressesAvailable()
internalUiState.value = stateFactory.getStateWithUpdatedMenu(
userWallet = userWallet,
hasDerivations = hasDerivations,
isSupported = isSupported,
isDynamicAddressesAvailable = isDynamicAddressesAvailable,
)
}
}
private fun isDynamicAddressesAvailable(): Boolean {
if (!dynamicAddressesFeatureToggles.isDynamicAddressesEnabled) return false
if (cryptoCurrency !is CryptoCurrency.Coin) return false
val networkId = cryptoCurrency.network.rawId
if (!DynamicAddressesSupportedBlockchains.isSupportedByNetworkId(networkId)) return false
return isDefaultBaseDerivation(cryptoCurrency.network.derivationPath, networkId)
}
private fun isDefaultBaseDerivation(derivationPath: Network.DerivationPath, networkId: String): Boolean {
val pathValue = derivationPath.value ?: return false
val nodes = runCatching { DerivationPath(pathValue).nodes }.getOrNull() ?: return false
if (nodes.size < BASE_DERIVATION_NODE_COUNT) return false
val purposeNode = nodes.first()
val allowedPurpose = DynamicAddressesSupportedBlockchains.getAllowedPurpose(networkId) ?: return false
if (purposeNode.getIndex(includeHardened = false) != allowedPurpose) return false
val changeNode = nodes[nodes.size - 2]
val indexNode = nodes.last()
return changeNode.getIndex(includeHardened = false) == 0L &&
indexNode.getIndex(includeHardened = false) == 0L
}
private suspend fun isXPUBSupported(): Boolean {
return getExtendedPublicKeyForCurrencyUseCase.isSupported(
userWalletId = userWalletId,
network = cryptoCurrency.network,
)
.mapLeft { throwable ->
analyticsExceptionHandler.sendException(
event = ExceptionAnalyticsEvent(
exception = throwable,
params = mapOf(
"blockchainId" to cryptoCurrency.network.id.rawId.value,
"networkId" to cryptoCurrency.network.backendId,
),
),
)
TangemLogger.e(
"Unable to get wallet manager for user wallet $userWalletId and network ${cryptoCurrency.network}",
throwable,
)
false
}
.merge()
return isXpubSupportedUseCase(userWalletId = userWalletId, network = cryptoCurrency.network)
}
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
@ -657,6 +693,15 @@ internal class TokenDetailsModel @Inject constructor(
openStaking()
}
override fun onDynamicAddressesClick() = dynamicAddressesDelegate.onDynamicAddressesClick()
private fun onDynamicAddressesEnabled() {
updateTopBarMenu()
modelScope.launch(dispatchers.main) {
cryptoCurrencyBalanceFetcher.invokeAndAwait(userWalletId = userWalletId, currency = cryptoCurrency)
}
}
override fun onGenerateExtendedKey() {
modelScope.launch(dispatchers.main) {
val extendedKey = getExtendedPublicKeyForCurrencyUseCase(
@ -1382,5 +1427,6 @@ internal class TokenDetailsModel @Inject constructor(
private companion object {
const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L
const val BASE_DERIVATION_NODE_COUNT = 5
}
}

View file

@ -27,4 +27,7 @@ sealed class TokenDetailsBottomSheetConfig : Route {
@Serializable
data object CloreMigration : TokenDetailsBottomSheetConfig()
@Serializable
data object DynamicAddresses : TokenDetailsBottomSheetConfig()
}

View file

@ -167,12 +167,18 @@ internal class TokenDetailsStateFactory(
userWallet: UserWallet,
hasDerivations: Boolean,
isSupported: Boolean,
isDynamicAddressesAvailable: Boolean = false,
): TokenDetailsState {
return with(currentStateProvider()) {
copy(
topAppBarConfig = topAppBarConfig.copy(
tokenDetailsAppBarMenuConfig = topAppBarConfig.tokenDetailsAppBarMenuConfig
?.updateMenu(userWallet, hasDerivations, isSupported),
?.updateMenu(
userWallet = userWallet,
hasDerivations = hasDerivations,
isSupported = isSupported,
isDynamicAddressesAvailable = isDynamicAddressesAvailable,
),
),
)
}
@ -206,6 +212,7 @@ internal class TokenDetailsStateFactory(
userWallet: UserWallet,
hasDerivations: Boolean,
isSupported: Boolean,
isDynamicAddressesAvailable: Boolean,
): TokenDetailsAppBarMenuConfig? {
if (userWallet is UserWallet.Cold &&
userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
@ -215,6 +222,13 @@ internal class TokenDetailsStateFactory(
return copy(
items = buildList {
if (isDynamicAddressesAvailable) {
TangemDropdownMenuItem(
title = resourceReference(R.string.dynamic_addresses),
textColor = themedColor { TangemTheme.colors.text.primary1 },
onClick = tokenDetailsClickIntents::onDynamicAddressesClick,
).let(::add)
}
if (isSupported && hasDerivations) {
TangemDropdownMenuItem(
title = resourceReference(R.string.token_details_generate_xpub),

View file

@ -0,0 +1,34 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.feature.tokendetails.presentation.tokendetails.model.DynamicAddressesDelegate
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.dynamicaddresses.DynamicAddressesBottomSheet
internal class DynamicAddressesBottomSheetComponent(
private val dynamicAddressesDelegate: DynamicAddressesDelegate,
private val onDismiss: () -> Unit,
) : ComposableBottomSheetComponent {
override fun dismiss() {
onDismiss()
}
@Composable
override fun BottomSheet() {
val content by dynamicAddressesDelegate.bottomSheetConfig.collectAsStateWithLifecycle()
val config = remember(content) {
TangemBottomSheetConfig(
isShown = true,
onDismissRequest = ::dismiss,
content = content,
)
}
DynamicAddressesBottomSheet(config = config)
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.dynamicaddresses
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.R as CoreR
@Composable
internal fun DynamicAddressesBottomSheet(config: TangemBottomSheetConfig) {
TangemModalBottomSheet<DynamicAddressesBottomSheetConfig>(
config = config,
title = {
TangemModalBottomSheetTitle(
endIconRes = CoreR.drawable.ic_close_24,
onEndClick = config.onDismissRequest,
)
},
) { content ->
when (content) {
is DynamicAddressesBottomSheetConfig.Enable -> DynamicAddressesEnableContent(content = content)
is DynamicAddressesBottomSheetConfig.Unavailable -> DynamicAddressesUnavailableContent(content = content)
is DynamicAddressesBottomSheetConfig.ServiceUnavailable -> DynamicAddressesServiceUnavailableContent(
content = content,
)
}
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.dynamicaddresses
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
@Immutable
internal sealed class DynamicAddressesBottomSheetConfig : TangemBottomSheetConfigContent {
data class Enable(
val isCardScanRequired: Boolean,
val isLoading: Boolean = false,
val onEnableClick: () -> Unit,
) : DynamicAddressesBottomSheetConfig()
data class Unavailable(
val onGotItClick: () -> Unit,
) : DynamicAddressesBottomSheetConfig()
data class ServiceUnavailable(
val onGotItClick: () -> Unit,
) : DynamicAddressesBottomSheetConfig()
}

View file

@ -0,0 +1,179 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.dynamicaddresses
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
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.style.TextAlign
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.res.R
import com.tangem.core.ui.R as CoreR
@Composable
internal fun DynamicAddressesEnableContent(content: DynamicAddressesBottomSheetConfig.Enable) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
painter = painterResource(id = CoreR.drawable.ic_dynamic_addresses_bottomsheet_enable_top),
contentDescription = null,
modifier = Modifier.size(TangemTheme.dimens.size44),
tint = TangemTheme.colors.icon.accent,
)
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing12))
Text(
text = stringResourceSafe(id = R.string.dynamic_addresses),
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_enter_subtitle),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing24))
FeatureItem(
iconRes = CoreR.drawable.ic_dynamic_addresses_bottomsheet_flash_24,
title = stringResourceSafe(id = R.string.dynamic_addresses_enter_features_receving_title),
description = stringResourceSafe(id = R.string.dynamic_addresses_enter_features_receving_description),
)
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing16))
FeatureItem(
iconRes = CoreR.drawable.ic_dynamic_addresses_bottomsheet_check_24,
title = stringResourceSafe(id = R.string.dynamic_addresses_enter_features_privacy_title),
description = stringResourceSafe(id = R.string.dynamic_addresses_enter_features_privacy_description),
)
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing24))
PrimaryButton(
text = stringResourceSafe(id = R.string.dynamic_addresses_enter_main_button_title),
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))
}
}
@Composable
internal fun DynamicAddressesUnavailableContent(content: DynamicAddressesBottomSheetConfig.Unavailable) {
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,
)
}
@Composable
internal fun DynamicAddressesServiceUnavailableContent(content: DynamicAddressesBottomSheetConfig.ServiceUnavailable) {
ErrorContent(
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,
)
}
@Composable
private fun ErrorContent(titleRes: Int, descriptionRes: Int, buttonTextRes: Int, onButtonClick: () -> Unit) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
painter = painterResource(id = CoreR.drawable.ic_dynamic_addresses_bottomsheet_enable_unavailable),
contentDescription = null,
modifier = Modifier.size(TangemTheme.dimens.size44),
tint = TangemTheme.colors.icon.warning,
)
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing12))
Text(
text = stringResourceSafe(id = titleRes),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing8))
Text(
text = stringResourceSafe(id = descriptionRes),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing24))
PrimaryButton(
text = stringResourceSafe(id = buttonTextRes),
onClick = onButtonClick,
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing16))
}
}
@Composable
private fun FeatureItem(iconRes: Int, title: String, description: String) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
modifier = Modifier.size(TangemTheme.dimens.size24),
tint = TangemTheme.colors.icon.accent,
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing4))
Text(
text = description,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
}