Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-18 21:55:50 +05:00
parent 50c3b57a91
commit 3d2ef29a24
13 changed files with 423 additions and 5 deletions

View file

@ -24,6 +24,7 @@ dependencies {
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.coil)
/** DI */
implementation(deps.hilt.android)
@ -32,6 +33,7 @@ dependencies {
/** Core modules */
implementation(projects.core.featuretoggles)
implementation(projects.core.ui)
implementation(projects.core.navigation)
/** Feature Apis */
implementation(projects.features.tokendetails.api)

View file

@ -1,5 +1,6 @@
package com.tangem.feature.tokendetails.di
import com.tangem.core.navigation.NavigationStateHolder
import com.tangem.feature.tokendetails.presentation.router.DefaultTokenDetailsRouter
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import dagger.Module
@ -14,5 +15,7 @@ internal object TokenDetailsRouterModule {
@Provides
@ActivityScoped
fun provideTokenDetailsRouter(): TokenDetailsRouter = DefaultTokenDetailsRouter()
fun provideTokenDetailsRouter(navigationStateHolder: NavigationStateHolder): TokenDetailsRouter {
return DefaultTokenDetailsRouter(navigationStateHolder)
}
}

View file

@ -0,0 +1,46 @@
package com.tangem.feature.tokendetails.presentation
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.hilt.navigation.compose.hiltViewModel
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsViewModel
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class TokenDetailsFragment : Fragment() {
@Inject
lateinit var tokenDetailsRouter: TokenDetailsRouter
private val internalTokenDetailsRouter: InnerTokenDetailsRouter
get() = requireNotNull(tokenDetailsRouter as? InnerTokenDetailsRouter) {
"internalTokenDetailsRouter should be instance of InnerTokenDetailsRouter"
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(inflater.context).apply {
setContent {
TangemTheme {
val systemBarsColor = TangemTheme.colors.background.secondary
SystemBarsEffect {
setSystemBarsColor(systemBarsColor)
}
val viewModel = hiltViewModel<TokenDetailsViewModel>()
viewModel.router = this@TokenDetailsFragment.internalTokenDetailsRouter
TokenDetailsScreen(state = viewModel.uiState)
}
}
}
}
}

View file

@ -1,10 +1,17 @@
package com.tangem.feature.tokendetails.presentation.router
import androidx.fragment.app.Fragment
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.NavigationStateHolder
import com.tangem.feature.tokendetails.presentation.TokenDetailsFragment
internal class DefaultTokenDetailsRouter : TokenDetailsRouter {
internal class DefaultTokenDetailsRouter(
private val navigationStateHolder: NavigationStateHolder,
) : InnerTokenDetailsRouter {
// TODO: Doston fix it in next PRs
override fun getEntryFragment(): Fragment = Fragment()
override fun getEntryFragment(): Fragment = TokenDetailsFragment()
override fun popBackStack() {
navigationStateHolder.navigate(NavigationAction.PopBackTo())
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.feature.tokendetails.presentation.router
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
internal interface InnerTokenDetailsRouter : TokenDetailsRouter {
fun popBackStack()
}

View file

@ -0,0 +1,41 @@
package com.tangem.feature.tokendetails.presentation.tokendetails
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState
import com.tangem.features.tokendetails.impl.R
internal object TokenDetailsPreviewData {
val tokenDetailsTopAppBarConfig = TokenDetailsTopAppBarConfig(onBackClick = {}, onMoreClick = {})
val tokenInfoBlockStateWithLongNameInMainCurrency = TokenInfoBlockState(
name = "Stellar (XLM) with long name test",
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png",
currency = TokenInfoBlockState.Currency.Native,
)
val tokenInfoBlockStateWithLongName = TokenInfoBlockState(
name = "Tether (USDT) with long name test",
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png",
currency = TokenInfoBlockState.Currency.Token(
networkName = "ERC20",
networkIcon = R.drawable.img_eth_22,
blockchainName = "Ethereum",
),
)
val tokenInfoBlockState = TokenInfoBlockState(
name = "Tether USDT",
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png",
currency = TokenInfoBlockState.Currency.Token(
networkName = "ERC20",
networkIcon = R.drawable.img_eth_22,
blockchainName = "Ethereum",
),
)
val tokenDetailsState = TokenDetailsState(
topAppBarConfig = tokenDetailsTopAppBarConfig,
tokenInfoBlockState = tokenInfoBlockState,
)
}

View file

@ -0,0 +1,6 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
data class TokenDetailsState(
val topAppBarConfig: TokenDetailsTopAppBarConfig,
val tokenInfoBlockState: TokenInfoBlockState,
)

View file

@ -0,0 +1,6 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
data class TokenDetailsTopAppBarConfig(
val onBackClick: () -> Unit,
val onMoreClick: () -> Unit,
)

View file

@ -0,0 +1,23 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import androidx.annotation.DrawableRes
data class TokenInfoBlockState(
val name: String,
val iconUrl: String,
val currency: Currency,
) {
sealed class Currency {
object Native : Currency()
/**
* @param networkName - token standard. Samples: ERC20, BEP20, BEP2, TRC20 and etc.
* @param blockchainName - token's blockchain name. Ethereum, Tron and etc.
*/
data class Token(
val networkName: String,
val blockchainName: String,
@DrawableRes val networkIcon: Int,
) : Currency()
}
}

View file

@ -0,0 +1,47 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock
@Composable
internal fun TokenDetailsScreen(state: TokenDetailsState) {
Scaffold(
topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) },
containerColor = TangemTheme.colors.background.secondary,
) { scaffoldPaddings ->
Column(
modifier = Modifier
.padding(paddingValues = scaffoldPaddings)
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxSize(),
) {
TokenInfoBlock(state = state.tokenInfoBlockState)
}
}
}
@Preview
@Composable
private fun Preview_TokenDetailsScreen_LightTheme() {
TangemTheme(isDark = false) {
TokenDetailsScreen(state = TokenDetailsPreviewData.tokenDetailsState)
}
}
@Preview
@Composable
private fun Preview_TokenDetailsScreen_DarkTheme() {
TangemTheme(isDark = true) {
TokenDetailsScreen(state = TokenDetailsPreviewData.tokenDetailsState)
}
}

View file

@ -0,0 +1,58 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.features.tokendetails.impl.R
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) {
TopAppBar(
navigationIcon = {
IconButton(onClick = config.onBackClick) {
Icon(
painter = painterResource(id = R.drawable.ic_back_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = "Back",
)
}
},
title = {},
actions = {
IconButton(onClick = config.onMoreClick) {
Icon(
painter = painterResource(id = R.drawable.ic_more_vertical_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = "More",
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = TangemTheme.colors.background.secondary,
titleContentColor = TangemTheme.colors.icon.primary1,
actionIconContentColor = TangemTheme.colors.icon.primary1,
),
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
)
}
@Preview
@Composable
private fun Preview_TokenDetailsTopAppBar_LightTheme() {
TangemTheme(isDark = false) {
TokenDetailsTopAppBar(config = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig)
}
}
@Preview
@Composable
private fun Preview_TokenDetailsTopAppBar_DarkTheme() {
TangemTheme(isDark = true) {
TokenDetailsTopAppBar(config = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig)
}
}

View file

@ -0,0 +1,141 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import coil.compose.rememberAsyncImagePainter
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState
import com.tangem.features.tokendetails.impl.R
@Composable
internal fun TokenInfoBlock(state: TokenInfoBlockState, modifier: Modifier = Modifier) {
Row(modifier = modifier.fillMaxWidth()) {
Column(
modifier = Modifier.weight(1F),
) {
Text(
text = state.name,
style = TangemTheme.typography.h1,
color = TangemTheme.colors.text.primary1,
)
NetworkInfoText(state.currency)
}
val tokenIconPainter = when (LocalInspectionMode.current) {
// show drawable res in preview
true -> painterResource(id = R.drawable.img_stellar_22)
false -> rememberAsyncImagePainter(model = state.iconUrl)
}
Image(
modifier = Modifier.size(TangemTheme.dimens.size48),
painter = tokenIconPainter,
contentDescription = null,
)
}
}
@Composable
private fun NetworkInfoText(currency: TokenInfoBlockState.Currency) {
when (currency) {
TokenInfoBlockState.Currency.Native -> {
Text(
text = stringResource(id = R.string.common_main_network),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption,
)
}
is TokenInfoBlockState.Currency.Token -> {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
) {
val state = extractNetwork(tokenCurrency = currency)
Text(
text = state.normalText,
style = TangemTheme.typography.caption,
color = TangemTheme.colors.text.tertiary,
)
Icon(
modifier = Modifier.size(TangemTheme.dimens.size16),
painter = painterResource(id = currency.networkIcon),
tint = Color.Unspecified,
contentDescription = null,
)
Text(
text = state.boldText,
style = TangemTheme.typography.caption.copy(fontWeight = FontWeight.Medium),
color = TangemTheme.colors.text.primary1,
)
}
}
}
}
private const val SEPARATOR = " %image% "
@Composable
private fun extractNetwork(tokenCurrency: TokenInfoBlockState.Currency.Token): ExtractedTokenNetworkText {
val splitString = stringResource(
id = R.string.token_details_token_type_subtitle,
formatArgs = arrayOf(
tokenCurrency.networkName,
tokenCurrency.blockchainName,
),
).split(SEPARATOR)
return remember(splitString) {
ExtractedTokenNetworkText(
normalText = splitString.firstOrNull().orEmpty(),
boldText = splitString.getOrNull(1).orEmpty(),
)
}
}
private data class ExtractedTokenNetworkText(val normalText: String, val boldText: String)
@Preview
@Composable
private fun Preview_TokenInfoBlock_LightTheme(
@PreviewParameter(TokenInfoStateProvider::class)
state: TokenInfoBlockState,
) {
TangemTheme(isDark = false) {
TokenInfoBlock(state, Modifier.background(TangemTheme.colors.background.secondary))
}
}
@Preview
@Composable
private fun Preview_TokenInfoBlock_DarkTheme(
@PreviewParameter(TokenInfoStateProvider::class)
state: TokenInfoBlockState,
) {
TangemTheme(isDark = true) {
TokenInfoBlock(state, Modifier.background(TangemTheme.colors.background.secondary))
}
}
private class TokenInfoStateProvider : CollectionPreviewParameterProvider<TokenInfoBlockState>(
collection = listOf(
TokenDetailsPreviewData.tokenInfoBlockState,
TokenDetailsPreviewData.tokenInfoBlockStateWithLongName,
TokenDetailsPreviewData.tokenInfoBlockStateWithLongNameInMainCurrency,
),
)

View file

@ -0,0 +1,30 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlin.properties.Delegates
@HiltViewModel
internal class TokenDetailsViewModel @Inject constructor() : ViewModel() {
var router: InnerTokenDetailsRouter by Delegates.notNull()
var uiState by mutableStateOf(getInitialState())
private set
private fun getInitialState() = TokenDetailsPreviewData.tokenDetailsState.copy(
topAppBarConfig = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig.copy(
onBackClick = ::onBackClick,
),
)
private fun onBackClick() {
router.popBackStack()
}
}