Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-11 14:49:08 +03:00
parent 0a6b47b668
commit 71038ca2d8
39 changed files with 1073 additions and 89 deletions

View file

@ -33,7 +33,6 @@ internal class PreviewDetailsComponent : DetailsComponent {
)
@Composable
@Suppress("TopLevelComposableFunctions") // TODO: Remove this check
override fun Content(modifier: Modifier) {
DetailsScreen(
modifier = modifier,

View file

@ -45,7 +45,6 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
)
@Composable
@Suppress("TopLevelComposableFunctions") // TODO: Remove this check
override fun Content(modifier: Modifier) {
UserWalletListBlock(state = previewState, modifier = modifier)
}

View file

@ -0,0 +1,5 @@
package com.tangem.features.markets
interface MarketsFeatureToggles {
val isFeatureEnabled: Boolean
}

View file

@ -1,12 +0,0 @@
package com.tangem.features.markets
import androidx.compose.runtime.Stable
@Stable
interface MarketsListComponent {
// TODO
// @Composable
// fun BottomSheetContent(state: State<BottomSheetState>, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier)
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.markets
package com.tangem.features.markets.component
enum class BottomSheetState {
EXPANDED,

View file

@ -0,0 +1,23 @@
package com.tangem.features.markets.component
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.tangem.core.decompose.context.AppComponentContext
@Stable
interface MarketsListComponent {
@Composable
fun BottomSheetContent(
bottomSheetState: State<BottomSheetState>,
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
)
interface Factory {
fun create(context: AppComponentContext): MarketsListComponent
}
}

View file

@ -1,6 +1,9 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.hilt.android)
id("configuration")
}
@ -9,16 +12,31 @@ android {
}
dependencies {
/* Project - API */
api(projects.features.markets.api)
/* Compose */
implementation(deps.compose.coil)
implementation(deps.compose.foundation)
implementation(deps.compose.material3)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.ui.utils)
implementation(deps.lifecycle.compose)
/* DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/* Other */
implementation(deps.kotlin.immutable.collections)
implementation(deps.timber)
/* Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
implementation(projects.core.featuretoggles)
implementation(projects.common.ui)
implementation(projects.common.uiCharts)
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.markets
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
internal class DefaultMarketsFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : MarketsFeatureToggles {
override val isFeatureEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("MARKETS_ENABLED")
}

View file

@ -0,0 +1,44 @@
package com.tangem.features.markets.component.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.markets.component.BottomSheetState
import com.tangem.features.markets.component.MarketsListComponent
import com.tangem.features.markets.model.MarketsListModel
import com.tangem.features.markets.ui.MarketsList
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultMarketsListComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
) : MarketsListComponent, AppComponentContext by context {
private val model: MarketsListModel = getOrCreateModel()
@Composable
override fun BottomSheetContent(
bottomSheetState: State<BottomSheetState>,
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
) {
val state by model.state.collectAsStateWithLifecycle()
MarketsList(
onHeaderSizeChange = onHeaderSizeChange,
state = state,
modifier = modifier,
)
}
@AssistedFactory
interface Factory : MarketsListComponent.Factory {
override fun create(context: AppComponentContext): DefaultMarketsListComponent
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.markets.di
import com.tangem.features.markets.component.MarketsListComponent
import com.tangem.features.markets.component.impl.DefaultMarketsListComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ComponentModule {
@Binds
@Singleton
fun bindMarketsListComponent(factory: DefaultMarketsListComponent.Factory): MarketsListComponent.Factory
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.markets.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.features.markets.DefaultMarketsFeatureToggles
import com.tangem.features.markets.MarketsFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object FeatureModule {
@Provides
@Singleton
fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): MarketsFeatureToggles =
DefaultMarketsFeatureToggles(featureTogglesManager = featureTogglesManager)
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.markets.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.markets.model.MarketsListModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(DecomposeComponent::class)
internal interface ModelModule {
@Binds
@IntoMap
@ClassKey(MarketsListModel::class)
fun provideMarketsListModel(model: MarketsListModel): Model
}

View file

@ -0,0 +1,61 @@
package com.tangem.features.markets.model
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.ui.entity.ListUM
import com.tangem.features.markets.ui.entity.MarketsListUM
import com.tangem.features.markets.ui.entity.SortByTypeUM
import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@ComponentScoped
internal class MarketsListModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
val state = MutableStateFlow(
MarketsListUM(
list = ListUM.Loading,
searchBar = SearchBarUM(
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
query = "",
onQueryChange = {},
isActive = false,
onActiveChange = { },
),
selectedSortBy = SortByTypeUM.Rating,
selectedInterval = MarketsListUM.TrendInterval.H24,
onIntervalClick = {},
onSortByButtonClick = {},
),
)
init {
modelScope.launch {
delay(timeMillis = 5000)
state.update {
it.copy(
list = ListUM.Content(
items = MarketChartListItemPreviewDataProvider().values
.flatMap { item -> List(size = 10) { item } }
.mapIndexed { index, item ->
item.copy(id = index.toString())
}
.toImmutableList(),
),
)
}
}
}
// TODO
}

View file

@ -0,0 +1,217 @@
package com.tangem.features.markets.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.ui.components.MarketsListItem
import com.tangem.features.markets.ui.components.MarketsListItemPlaceholder
import com.tangem.features.markets.ui.entity.ListUM
import com.tangem.features.markets.ui.entity.MarketsListUM
import com.tangem.features.markets.ui.entity.SortByTypeUM
import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun MarketsList(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) {
Content(
modifier = modifier,
state = state,
onHeaderSizeChange = onHeaderSizeChange,
)
}
@Composable
private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) {
val density = LocalDensity.current
Column(
modifier = modifier
.fillMaxSize()
.imePadding()
.background(color = TangemTheme.colors.background.primary),
) {
SearchBar(
state = state.searchBar,
modifier = Modifier
.background(color = TangemTheme.colors.background.primary)
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing4,
)
.onGloballyPositioned {
with(density) { onHeaderSizeChange(it.size.height.toDp()) }
},
)
Spacer(Modifier.height(TangemTheme.dimens.spacing20))
Column(Modifier.padding(horizontal = TangemTheme.dimens.size16)) {
Title()
SpacerH12()
Options(
sortByTypeUM = state.selectedSortBy,
trendInterval = state.selectedInterval,
onIntervalClick = state.onIntervalClick,
onSortByClick = state.onSortByButtonClick,
)
}
SpacerH12()
Items(state = state.list)
}
}
@Composable
private fun Title(modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = stringResource(id = R.string.markets_common_title),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
}
@Composable
private fun Options(
sortByTypeUM: SortByTypeUM,
trendInterval: MarketsListUM.TrendInterval,
onSortByClick: () -> Unit,
onIntervalClick: (MarketsListUM.TrendInterval) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.height(IntrinsicSize.Max)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
SecondarySmallButton(
config = SmallButtonConfig(
text = sortByTypeUM.text,
onClick = onSortByClick,
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
),
)
SegmentedButtons(
config = persistentListOf(
MarketsListUM.TrendInterval.H24,
MarketsListUM.TrendInterval.D7,
MarketsListUM.TrendInterval.M1,
),
initialSelectedItem = trendInterval,
onClick = onIntervalClick,
modifier = Modifier
.width(160.dp)
.fillMaxHeight(),
) {
Box(
Modifier
.fillMaxSize()
.align(Alignment.Center)
.padding(
vertical = TangemTheme.dimens.spacing4,
),
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = it.text.resolveReference(),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.primary1,
)
}
}
}
}
@Composable
private fun Items(state: ListUM, modifier: Modifier = Modifier) {
val lazyListState = rememberLazyListState()
val scrollEnabled = state !is ListUM.Loading
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
LazyColumn(
modifier = modifier,
state = lazyListState,
contentPadding = PaddingValues(bottom = bottomBarHeight),
userScrollEnabled = scrollEnabled,
) {
when (state) {
ListUM.Loading -> {
items(count = 50, key = { it }) {
MarketsListItemPlaceholder()
}
}
ListUM.SearchNothingFound -> {
// TODO
}
is ListUM.Content -> {
items(
items = state.items,
key = { it.id },
) { item ->
MarketsListItem(
model = item,
)
}
}
}
}
}
//region: Preview
@Preview
@Composable
private fun Preview() {
TangemThemePreview {
MarketsList(
state = MarketsListUM(
list = ListUM.Content(
items = MarketChartListItemPreviewDataProvider().values
.flatMap { item -> List(size = 10) { item } }
.mapIndexed { index, item ->
item.copy(id = index.toString())
}
.toImmutableList(),
),
searchBar = SearchBarUM(
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
query = "",
onQueryChange = {},
isActive = false,
onActiveChange = { },
),
selectedSortBy = SortByTypeUM.Rating,
selectedInterval = MarketsListUM.TrendInterval.H24,
onIntervalClick = {},
onSortByButtonClick = {},
),
onHeaderSizeChange = {},
)
}
}
//endregion: Preview

View file

@ -1,4 +1,4 @@
package com.tangem.features.markets.ui
package com.tangem.features.markets.ui.components
import android.content.res.Configuration
import androidx.compose.animation.Animatable
@ -45,7 +45,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.windowsize.WindowSizeType
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.ui.models.MarketsListItemModel
import com.tangem.features.markets.ui.entity.MarketsListItemUM
import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
@ -63,7 +63,7 @@ const val SWIPE_VELOCITY_THRESHOLD = 20f
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MarketsListItem(
model: MarketsListItemModel,
model: MarketsListItemUM,
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
onSwipeToAction: () -> Unit = {},
@ -184,7 +184,7 @@ fun MarketsListItem(
}
@Composable
private fun MarketsListItemContent(model: MarketsListItemModel, modifier: Modifier = Modifier) {
private fun MarketsListItemContent(model: MarketsListItemUM, modifier: Modifier = Modifier) {
val windowSize = LocalWindowSize.current
Row(
@ -303,7 +303,7 @@ private fun RowScope.TokenRatingPlace(ratingPosition: String?) {
color = TangemTheme.colors.field.primary,
shape = TangemTheme.shapes.roundedCornersSmall2,
)
.padding(horizontal = TangemTheme.dimens.size5), // TODO check
.padding(horizontal = TangemTheme.dimens.spacing5),
) {
Text(
text = ratingPosition ?: "-",
@ -379,8 +379,9 @@ private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawD
RectangleShimmer(
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size15) // TODO check
.height(TangemTheme.dimens.size12)
.align(Alignment.Center),
radius = TangemTheme.dimens.radius3,
)
}
}
@ -391,7 +392,7 @@ private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawD
@Preview(showBackground = true, widthDp = 360, name = "normal night", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(showBackground = true, widthDp = 320, name = "small width")
@Composable
private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::class) state: MarketsListItemModel) {
private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::class) state: MarketsListItemUM) {
TangemThemePreview {
var state1 by remember { mutableStateOf(state) }
var state2 by remember { mutableStateOf(state) }
@ -435,13 +436,13 @@ private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::cl
}
}
state1 = state1.copy(
price = MarketsListItemModel.Price(
price = MarketsListItemUM.Price(
text = "0.${prices[0].first}023 $",
changeType = prices[0].second,
),
)
state2 = state2.copy(
price = MarketsListItemModel.Price(
price = MarketsListItemUM.Price(
text = "0.${prices[1].first}023 $",
changeType = prices[1].second,
),

View file

@ -0,0 +1,110 @@
package com.tangem.features.markets.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.windowsize.WindowSizeType
@Suppress("LongMethod")
@Composable
fun MarketsListItemPlaceholder() {
val density = LocalDensity.current
val windowSize = LocalWindowSize.current
val sp12 = with(density) { 12.sp.toDp() }
Row(
modifier = Modifier.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing15,
),
verticalAlignment = Alignment.CenterVertically,
) {
CircleShimmer(Modifier.size(TangemTheme.dimens.size36))
SpacerW12()
Column(modifier = Modifier.weight(1f)) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens.spacing4),
horizontalArrangement = Arrangement.SpaceBetween,
) {
RectangleShimmer(
modifier = Modifier
.width(TangemTheme.dimens.size70)
.height(sp12),
radius = TangemTheme.dimens.radius3,
)
SpacerW8()
RectangleShimmer(
modifier = Modifier
.width(TangemTheme.dimens.size70)
.height(sp12),
radius = TangemTheme.dimens.radius3,
)
}
SpacerH(height = TangemTheme.dimens.spacing2)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens.spacing2),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom,
) {
RectangleShimmer(
modifier = Modifier
.width(TangemTheme.dimens.size52)
.height(sp12),
radius = TangemTheme.dimens.radius3,
)
RectangleShimmer(
modifier = Modifier
.width(TangemTheme.dimens.size52)
.height(sp12),
radius = TangemTheme.dimens.radius3,
)
}
}
if (windowSize.widthAtLeast(WindowSizeType.Small)) {
Spacer(Modifier.width(TangemTheme.dimens.spacing10))
Box {
RectangleShimmer(
modifier = Modifier
.align(Alignment.Center)
.width(TangemTheme.dimens.size56)
.height(TangemTheme.dimens.size12),
radius = TangemTheme.dimens.radius3,
)
}
}
}
}
@Preview(showBackground = true, widthDp = 360, name = "normal")
@Preview(showBackground = true, widthDp = 360, name = "normal night", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(showBackground = true, widthDp = 320, name = "small width")
@Composable
private fun Preview() {
TangemThemePreview {
Column(Modifier.background(TangemTheme.colors.background.tertiary)) {
repeat(20) {
MarketsListItemPlaceholder()
}
}
}
}

View file

@ -0,0 +1,3 @@
package com.tangem.features.markets.ui.entity
sealed class ListItem

View file

@ -1,4 +1,4 @@
package com.tangem.features.markets.ui.models
package com.tangem.features.markets.ui.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.charts.state.MarketChartLook
@ -6,7 +6,7 @@ import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.core.ui.components.marketprice.PriceChangeType
@Immutable
data class MarketsListItemModel(
data class MarketsListItemUM(
val id: String,
val name: String,
val currencySymbol: String,

View file

@ -0,0 +1,42 @@
package com.tangem.features.markets.ui.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.ImmutableList
internal data class MarketsListUM(
val list: ListUM,
val searchBar: SearchBarUM,
val selectedSortBy: SortByTypeUM,
val selectedInterval: TrendInterval,
val onIntervalClick: (TrendInterval) -> Unit,
val onSortByButtonClick: () -> Unit,
) {
enum class TrendInterval(val text: TextReference) {
H24(resourceReference(R.string.markets_selector_interval_24h_title)),
D7(resourceReference(R.string.markets_selector_interval_7d_title)),
M1(resourceReference(R.string.markets_selector_interval_1m_title)),
}
}
enum class SortByTypeUM(val text: TextReference) {
Rating(resourceReference(R.string.markets_sort_by_rating_title)),
Trending(resourceReference(R.string.markets_sort_by_trending_title)),
ExperiencedBuyers(resourceReference(R.string.markets_sort_by_experienced_buyers_title)),
TopGainers(resourceReference(R.string.markets_sort_by_top_gainers_title)),
TopLosers(resourceReference(R.string.markets_sort_by_top_losers_title)),
}
@Immutable
sealed class ListUM {
data class Content(
val items: ImmutableList<MarketsListItemUM>,
) : ListUM()
data object Loading : ListUM()
data object SearchNothingFound : ListUM()
}

View file

@ -4,86 +4,86 @@ package com.tangem.features.markets.ui.preview
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.features.markets.ui.models.MarketsListItemModel
import com.tangem.features.markets.ui.entity.MarketsListItemUM
internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider<MarketsListItemModel>(
internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider<MarketsListItemUM>(
collection = listOf(
MarketsListItemModel(
MarketsListItemUM(
id = "1",
name = "Bitcoin",
currencySymbol = "BTC",
iconUrl = "",
ratingPosition = "10",
marketCap = "$6.233 B",
price = MarketsListItemModel.Price(text = "31 285.72$"),
price = MarketsListItemUM.Price(text = "31 285.72$"),
trendPercentText = "12.43%",
trendType = PriceChangeType.UP,
chardData = MarketChartRawData(
y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f),
),
),
MarketsListItemModel(
MarketsListItemUM(
id = "1",
name = "Bitcoin",
currencySymbol = "BTC",
iconUrl = null,
ratingPosition = "10",
marketCap = "$6.233 B",
price = MarketsListItemModel.Price(text = "31 285.72$"),
price = MarketsListItemUM.Price(text = "31 285.72$"),
trendPercentText = "12.43%",
trendType = PriceChangeType.NEUTRAL,
chardData = null,
),
MarketsListItemModel(
MarketsListItemUM(
id = "1",
name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin",
currencySymbol = "BTC",
iconUrl = null,
ratingPosition = "10",
marketCap = "$6.23348172384781234 B",
price = MarketsListItemModel.Price(text = "31 285.72$"),
price = MarketsListItemUM.Price(text = "31 285.72$"),
trendPercentText = "12.43%",
trendType = PriceChangeType.DOWN,
chardData = MarketChartRawData(
y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f),
),
),
MarketsListItemModel(
MarketsListItemUM(
id = "1",
name = "Bitcoin",
currencySymbol = "BTC",
iconUrl = null,
ratingPosition = "10",
marketCap = null,
price = MarketsListItemModel.Price(text = "31 285.72$"),
price = MarketsListItemUM.Price(text = "31 285.72$"),
trendPercentText = "12.43%",
trendType = PriceChangeType.UP,
chardData = MarketChartRawData(
y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f),
),
),
MarketsListItemModel(
MarketsListItemUM(
id = "1",
name = "Bitcoin",
currencySymbol = "BTC",
iconUrl = null,
ratingPosition = null,
marketCap = "$6.233 B",
price = MarketsListItemModel.Price(text = "31 285.72$"),
price = MarketsListItemUM.Price(text = "31 285.72$"),
trendPercentText = "12.43%",
trendType = PriceChangeType.UP,
chardData = MarketChartRawData(
y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f),
),
),
MarketsListItemModel(
MarketsListItemUM(
id = "1",
name = "Bitcoin",
currencySymbol = "BTC",
iconUrl = null,
ratingPosition = null,
marketCap = null,
price = MarketsListItemModel.Price(text = "31 285.72$"),
price = MarketsListItemUM.Price(text = "31 285.72$"),
trendPercentText = "12.43%",
trendType = PriceChangeType.UP,
chardData = MarketChartRawData(

View file

@ -1407,7 +1407,7 @@ internal class StateBuilder(
return NotificationConfig(
title = resourceReference(R.string.express_provider_permission_needed),
subtitle = resourceReference(
id = R.string.give_permission_subtitle,
id = R.string.give_permission_swap_subtitle,
formatArgs = wrappedList(fromTokenSymbol),
),
iconResId = R.drawable.ic_locked_24,

View file

@ -55,6 +55,7 @@ dependencies {
implementation(projects.common.routing)
implementation(projects.core.deepLinks)
implementation(projects.core.deepLinks.global)
implementation(projects.core.decompose)
implementation(projects.libs.crypto)

View file

@ -1,11 +1,18 @@
package com.tangem.feature.wallet.presentation
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.defaultComponentContext
import com.tangem.core.decompose.context.DefaultAppComponentContext
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import com.tangem.features.markets.MarketsFeatureToggles
import com.tangem.features.markets.component.MarketsListComponent
import com.tangem.features.wallet.navigation.WalletRouter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@ -24,15 +31,45 @@ internal class WalletFragment : ComposeFragment() {
@Inject
internal lateinit var walletRouter: WalletRouter
@Inject
internal lateinit var marketsListComponentFactory: MarketsListComponent.Factory
@Inject
internal lateinit var coroutineDispatcherProvider: CoroutineDispatcherProvider
@Inject
internal lateinit var componentBuilder: DecomposeComponent.Builder
@Inject
internal lateinit var marketsFeatureToggles: MarketsFeatureToggles
private var marketsListComponent: MarketsListComponent? = null
private val _walletRouter: InnerWalletRouter
get() = requireNotNull(walletRouter as? InnerWalletRouter) {
"_walletRouter should be instance of InnerWalletRouter"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (marketsFeatureToggles.isFeatureEnabled) {
val appContext = DefaultAppComponentContext(
componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher),
messageHandler = uiDependencies.eventMessageHandler,
dispatchers = coroutineDispatcherProvider,
hiltComponentBuilder = componentBuilder,
)
marketsListComponent = marketsListComponentFactory.create(appContext)
}
}
@Composable
override fun ScreenContent(modifier: Modifier) {
_walletRouter.Initialize(
onFinish = requireActivity()::finish,
marketsListComponent = marketsListComponent,
)
}

View file

@ -25,6 +25,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScree
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel
import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel
import com.tangem.features.markets.component.MarketsListComponent
import kotlin.properties.Delegates
/** Default implementation of wallet feature router */
@ -40,7 +41,7 @@ internal class DefaultWalletRouter(
override fun getEntryFragment(): Fragment = WalletFragment.create()
@Composable
override fun Initialize(onFinish: () -> Unit) {
override fun Initialize(onFinish: () -> Unit, marketsListComponent: MarketsListComponent?) {
this.onFinish = onFinish
NavHost(
@ -55,6 +56,7 @@ internal class DefaultWalletRouter(
WalletScreen(
state = viewModel.uiState.collectAsStateWithLifecycle().value,
marketsListComponent = marketsListComponent,
)
}

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.markets.component.MarketsListComponent
import com.tangem.features.wallet.navigation.WalletRouter
/**
@ -22,9 +23,8 @@ internal interface InnerWalletRouter : WalletRouter {
*
* @param onFinish finish activity callback
*/
@Suppress("TopLevelComposableFunctions")
@Composable
fun Initialize(onFinish: () -> Unit)
fun Initialize(onFinish: () -> Unit, marketsListComponent: MarketsListComponent?)
/** Pop back stack */
fun popBackStack()

View file

@ -2,6 +2,11 @@ package com.tangem.feature.wallet.presentation.wallet.ui
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.TweenSpec
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
@ -10,27 +15,36 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.components.atoms.handComposableComponentHeight
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar
import com.tangem.core.ui.components.snackbar.TangemSnackbar
import com.tangem.core.ui.components.transactions.state.TxHistoryState
@ -55,10 +69,13 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.VisaTxDe
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balancesAndLimitsBlock
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.depositButton
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
import com.tangem.features.markets.component.BottomSheetState
import com.tangem.features.markets.component.MarketsListComponent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
@Composable
internal fun WalletScreen(state: WalletScreenState) {
internal fun WalletScreen(state: WalletScreenState, marketsListComponent: MarketsListComponent?) {
BackHandler(onBack = state.onBackClick)
// It means that screen is still initializing
@ -81,6 +98,8 @@ internal fun WalletScreen(state: WalletScreenState) {
snackbarHostState = snackbarHostState,
isAutoScroll = isAutoScroll,
onAutoScrollReset = { isAutoScroll.value = false },
marketsListComponent = marketsListComponent,
alertConfig = alertConfig,
)
WalletEventEffect(
@ -100,6 +119,8 @@ private fun WalletContent(
walletsListState: LazyListState,
snackbarHostState: SnackbarHostState,
isAutoScroll: State<Boolean>,
marketsListComponent: MarketsListComponent?,
alertConfig: WalletAlertState?,
onAutoScrollReset: () -> Unit,
) {
var selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) }
@ -195,12 +216,39 @@ private fun WalletContent(
)
}
BaseScaffold(
state = state,
selectedWallet = selectedWallet,
snackbarHostState = snackbarHostState,
) {
scaffoldContent()
if (marketsListComponent != null) {
val bottomSheetState = remember {
mutableStateOf(BottomSheetState.COLLAPSED)
}
var headerSize by remember {
mutableStateOf(0.dp)
}
BaseScaffoldWithMarkets(
state = state,
selectedWallet = selectedWallet,
snackbarHostState = snackbarHostState,
bottomSheetHeaderHeightProvider = { headerSize },
alertConfig = alertConfig,
onBottomSheetStateChange = { bottomSheetState.value = it },
bottomSheetContent = {
marketsListComponent.BottomSheetContent(
bottomSheetState = bottomSheetState,
onHeaderSizeChange = { headerSize = it },
modifier = Modifier,
)
},
) {
scaffoldContent()
}
} else {
BaseScaffold(
state = state,
selectedWallet = selectedWallet,
snackbarHostState = snackbarHostState,
) {
scaffoldContent()
}
}
}
@ -265,6 +313,268 @@ private fun BaseScaffold(
)
}
@Suppress("LongParameterList", "LongMethod")
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
@Composable
private fun BaseScaffoldWithMarkets(
state: WalletScreenState,
selectedWallet: WalletState,
snackbarHostState: SnackbarHostState,
bottomSheetHeaderHeightProvider: () -> Dp,
bottomSheetContent: @Composable () -> Unit,
alertConfig: WalletAlertState?,
onBottomSheetStateChange: (BottomSheetState) -> Unit,
content: @Composable () -> Unit,
) {
// show the bottom sheet if there is at least one multicurrency wallet
val showManageTokensBottomSheet = remember(state.wallets) {
state.wallets.any { it is WalletState.MultiCurrency }
}
val bottomSheetState = rememberSheetStateEnhanced(
initialValue = if (showManageTokensBottomSheet) SheetValue.PartiallyExpanded else SheetValue.Hidden,
confirmValueChange = { sheetValue ->
when {
sheetValue == SheetValue.Hidden && showManageTokensBottomSheet -> false
sheetValue != SheetValue.Hidden && !showManageTokensBottomSheet -> false
else -> true
}
},
skipHiddenState = showManageTokensBottomSheet,
)
val keyboardShown = keyboardAsState()
BottomSheetStateEffects(
bottomSheetState = bottomSheetState,
showManageTokensBottomSheet = showManageTokensBottomSheet,
alertConfig = alertConfig,
keyboardShown = keyboardShown,
onBottomSheetStateChange = onBottomSheetStateChange,
)
val scaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = bottomSheetState,
snackbarHostState = snackbarHostState,
)
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight
val coroutineScope = rememberCoroutineScope()
BottomSheetScaffold(
snackbarHost = {
WalletSnackbarHost(
snackbarHostState = it,
event = state.event,
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16),
)
},
containerColor = TangemTheme.colors.background.secondary,
sheetContainerColor = TangemTheme.colors.background.primary,
scaffoldState = scaffoldState,
sheetPeekHeight = peekHeight,
sheetDragHandle = {
Hand(modifier = Modifier.background(color = TangemTheme.colors.background.primary))
},
sheetTonalElevation = 8.dp,
sheetShadowElevation = 8.dp,
sheetContent = {
BoxWithConstraints {
Box(
modifier = Modifier
.sizeIn(maxHeight = maxHeight - statusBarHeight)
.align(Alignment.BottomCenter),
) {
bottomSheetContent()
}
}
// hide bottom sheet when back pressed
BackHandler(
keyboardShown.value is Keyboard.Closed &&
bottomSheetState.currentValue == SheetValue.Expanded,
) {
coroutineScope.launch { bottomSheetState.partialExpand() }
}
},
content = { _ ->
val pullRefreshState = rememberPullRefreshState(
refreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
onRefresh = {
selectedWallet.pullToRefreshConfig.onRefresh(WalletPullToRefreshConfig.ShowRefreshState(true))
},
)
Column {
WalletTopBar(config = state.topBarConfig)
Box(
modifier = Modifier.pullRefresh(pullRefreshState),
) {
content()
WalletPullToRefreshIndicator(
isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
)
}
}
BottomSheetScrim(
color = BottomSheetDefaults.ScrimColor,
visible = bottomSheetState.targetValue == SheetValue.Expanded,
onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } },
)
},
)
}
@Composable
private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: () -> Unit) {
val alpha by animateFloatAsState(
targetValue = if (visible) 1f else 0f,
animationSpec = TweenSpec(),
label = "scrim",
)
val dismissSheet = if (visible) {
Modifier
.pointerInput(onDismissRequest) {
detectTapGestures {
onDismissRequest()
}
}
.clearAndSetSemantics {}
} else {
Modifier
}
Canvas(
Modifier
.fillMaxSize()
.then(dismissSheet),
) {
drawRect(color = color, alpha = alpha)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("CyclomaticComplexMethod", "MagicNumber", "LongMethod")
@Composable
private fun BottomSheetStateEffects(
bottomSheetState: SheetState,
showManageTokensBottomSheet: Boolean,
alertConfig: WalletAlertState?,
keyboardShown: State<Keyboard>,
onBottomSheetStateChange: (BottomSheetState) -> Unit,
) {
// Bottom sheet during initialization internally expand partially after its content was remeasured,
// therefore initialValue = SheetValue.Hidden in rememberStandardBottomSheetState doesn't work as expected
// so we have to manually restrict expansion in this case
LaunchedEffect(bottomSheetState.targetValue, bottomSheetState.currentValue) {
if (!showManageTokensBottomSheet &&
(bottomSheetState.targetValue != SheetValue.Hidden || bottomSheetState.currentValue != SheetValue.Hidden)
) {
bottomSheetState.hide()
}
}
// react to changes in wallet list
LaunchedEffect(showManageTokensBottomSheet) {
when {
showManageTokensBottomSheet && bottomSheetState.currentValue != SheetValue.PartiallyExpanded -> {
bottomSheetState.partialExpand()
}
!showManageTokensBottomSheet && bottomSheetState.targetValue != SheetValue.Hidden -> {
bottomSheetState.hide()
}
}
}
val systemUiController = rememberSystemUiController()
val navigationBarColor = TangemTheme.colors.background.primary
LaunchedEffect(key1 = bottomSheetState.targetValue, navigationBarColor) {
when (bottomSheetState.targetValue) {
SheetValue.Hidden,
SheetValue.Expanded,
-> systemUiController.setNavigationBarColor(
color = Color.Transparent,
darkIcons = navigationBarColor.luminance() > 0.5f,
navigationBarContrastEnforced = true,
)
SheetValue.PartiallyExpanded,
-> systemUiController.setNavigationBarColor(navigationBarColor)
}
}
DisposableEffect(showManageTokensBottomSheet) {
onDispose {
if (showManageTokensBottomSheet) {
systemUiController.setNavigationBarColor(
color = Color.Transparent,
darkIcons = navigationBarColor.luminance() > 0.5f,
navigationBarContrastEnforced = false,
)
}
}
}
// expand bottom sheet when keyboard appears
LaunchedEffect(keyboardShown.value is Keyboard.Opened) {
if (keyboardShown.value is Keyboard.Opened && alertConfig == null) {
bottomSheetState.expand()
}
}
val keyboardController = LocalSoftwareKeyboardController.current
// hide keyboard when bottom sheet is about to be hidden
LaunchedEffect(Unit) {
snapshotFlow {
bottomSheetState.currentValue == SheetValue.Expanded &&
bottomSheetState.targetValue == SheetValue.PartiallyExpanded
}.collect { sheetHasBeenHidden ->
if (sheetHasBeenHidden) {
keyboardController?.hide()
}
}
}
val isSheetHidden = bottomSheetState.targetValue == SheetValue.PartiallyExpanded
LaunchedEffect(isSheetHidden) {
onBottomSheetStateChange(
if (isSheetHidden) {
BottomSheetState.COLLAPSED
} else {
BottomSheetState.EXPANDED
},
)
}
}
/**
* Use a standard method when this is fixed https://issuetracker.google.com/issues/314796718
* Current material3 version: 1.2.0
*/
@Composable
@ExperimentalMaterial3Api
private fun rememberSheetStateEnhanced(
skipPartiallyExpanded: Boolean = false,
confirmValueChange: (SheetValue) -> Boolean = { true },
initialValue: SheetValue = SheetValue.Hidden,
skipHiddenState: Boolean = false,
): SheetState {
val density = LocalDensity.current
return remember(initialValue, skipPartiallyExpanded, confirmValueChange, skipHiddenState) {
SheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
density = density,
initialValue = initialValue,
confirmValueChange = confirmValueChange,
skipHiddenState = skipHiddenState,
)
}
}
@Composable
private fun WalletSnackbarHost(
snackbarHostState: SnackbarHostState,
@ -328,6 +638,7 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider::
TangemThemePreview {
WalletScreen(
state = data,
marketsListComponent = null,
)
}
}