Updated on 2026-08-14
This commit is contained in:
parent
52a607c1f5
commit
9944036c4d
29 changed files with 874 additions and 60 deletions
|
|
@ -68,6 +68,8 @@ dependencies {
|
|||
implementation(projects.domain.balanceHiding.models)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.demo)
|
||||
implementation(projects.domain.dynamicAddresses)
|
||||
implementation(projects.domain.dynamicAddresses.models)
|
||||
implementation(projects.domain.markets.models)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.notifications.models)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 */ }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -27,4 +27,7 @@ sealed class TokenDetailsBottomSheetConfig : Route {
|
|||
|
||||
@Serializable
|
||||
data object CloreMigration : TokenDetailsBottomSheetConfig()
|
||||
|
||||
@Serializable
|
||||
data object DynamicAddresses : TokenDetailsBottomSheetConfig()
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue