diff --git a/features/yield-supply/impl/build.gradle.kts b/features/yield-supply/impl/build.gradle.kts index f2777b5baf..02c2af1cb6 100644 --- a/features/yield-supply/impl/build.gradle.kts +++ b/features/yield-supply/impl/build.gradle.kts @@ -22,6 +22,11 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.navigation) + /** Compose */ + implementation(tangemDeps.vico.core) + implementation(tangemDeps.vico.compose) + implementation(tangemDeps.vico.compose.m3) + /** Common */ implementation(projects.common.ui) implementation(projects.common.routing) diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/chart/DefaultYieldSupplyChartComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/chart/DefaultYieldSupplyChartComponent.kt new file mode 100644 index 0000000000..0fb4d6b26a --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/chart/DefaultYieldSupplyChartComponent.kt @@ -0,0 +1,35 @@ +package com.tangem.features.yield.supply.impl.chart + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.yield.supply.impl.chart.model.YieldSupplyChartModel +import com.tangem.features.yield.supply.impl.chart.ui.YieldSupplyChartContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultYieldSupplyChartComponent @AssistedInject constructor( + @Assisted private val appComponentContext: AppComponentContext, + @Assisted private val params: YieldSupplyChartModel.Params, +) : AppComponentContext by appComponentContext { + + private val model: YieldSupplyChartModel = getOrCreateModel(params = params) + + @Composable + fun Content(modifier: Modifier = Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + YieldSupplyChartContent( + state = state, + modifier = modifier, + ) + } + + @AssistedFactory + internal interface Factory { + fun create(context: AppComponentContext, params: YieldSupplyChartModel.Params): DefaultYieldSupplyChartComponent + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/chart/di/YieldSupplyChartModule.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/chart/di/YieldSupplyChartModule.kt new file mode 100644 index 0000000000..a724d05b01 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/chart/di/YieldSupplyChartModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.yield.supply.impl.chart.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.yield.supply.impl.chart.model.YieldSupplyChartModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface YieldSupplyChartModule { + + @Binds + @IntoMap + @ClassKey(YieldSupplyChartModel::class) + fun bindYieldSupplyChartModel(impl: YieldSupplyChartModel): Model +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/chart/entity/YieldSupplyChartUM.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/chart/entity/YieldSupplyChartUM.kt new file mode 100644 index 0000000000..c642e0745e --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/chart/entity/YieldSupplyChartUM.kt @@ -0,0 +1,36 @@ +package com.tangem.features.yield.supply.impl.chart.entity + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +@Immutable +internal sealed class YieldSupplyChartUM { + + @Immutable + data object Loading : YieldSupplyChartUM() + + @Immutable + data class Error(val onRetry: () -> Unit) : YieldSupplyChartUM() + + @Immutable + data class Data(val chartData: YieldSupplyMarketChartData) : YieldSupplyChartUM() +} + +@Immutable +internal data class YieldSupplyMarketChartData( + val y: ImmutableList, + val x: ImmutableList, +) { + companion object { + @Suppress("MagicNumber") + fun mock(): YieldSupplyMarketChartData { + // TODO [REDACTED_TASK_KEY] replace mock values with real APY series + val y = listOf( + 4.6, 4.0, 4.2, 3.3, 2.5, 5.6, 4.2, 6.7, 3.5, 2.5, 4.5, 3.4, + ).toImmutableList() + val x = List(y.size) { 1.0 }.toImmutableList() + return YieldSupplyMarketChartData(y = y, x = x) + } + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/chart/model/YieldSupplyChartModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/chart/model/YieldSupplyChartModel.kt new file mode 100644 index 0000000000..cedb9e80d5 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/chart/model/YieldSupplyChartModel.kt @@ -0,0 +1,36 @@ +package com.tangem.features.yield.supply.impl.chart.model + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.yield.supply.impl.chart.entity.YieldSupplyChartUM +import com.tangem.features.yield.supply.impl.chart.entity.YieldSupplyMarketChartData +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@Suppress("UnusedPrivateProperty") +@ModelScoped +internal class YieldSupplyChartModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + data class Params( + val tokenContractAddress: String, + val onRetry: () -> Unit = {}, + ) + + private val params: Params = paramsContainer.require() + + val uiState: StateFlow + field = MutableStateFlow(YieldSupplyChartUM.Loading) + + init { + // TODO [REDACTED_TASK_KEY] + val chartData = YieldSupplyMarketChartData.mock() + uiState.update { YieldSupplyChartUM.Data(chartData = chartData) } + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/chart/ui/YieldSupplyChartContent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/chart/ui/YieldSupplyChartContent.kt new file mode 100644 index 0000000000..d9aef20d88 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/chart/ui/YieldSupplyChartContent.kt @@ -0,0 +1,205 @@ +package com.tangem.features.yield.supply.impl.chart.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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 com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLabelComponent +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottomAxis +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberCustomStartAxis +import com.patrykandpatrick.vico.compose.cartesian.decoration.rememberHorizontalLine +import com.patrykandpatrick.vico.compose.cartesian.fullWidth +import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart +import com.patrykandpatrick.vico.compose.cartesian.rememberVicoScrollState +import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberColumnCartesianLayer +import com.patrykandpatrick.vico.compose.common.component.rememberLineComponent +import com.patrykandpatrick.vico.compose.common.shape.dashed +import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout +import com.patrykandpatrick.vico.core.cartesian.Zoom +import com.patrykandpatrick.vico.core.cartesian.axis.AxisPosition +import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis +import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis +import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter +import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel +import com.patrykandpatrick.vico.core.cartesian.data.ColumnCartesianLayerModel +import com.patrykandpatrick.vico.core.common.shape.Shape +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.yield.supply.impl.chart.entity.YieldSupplyChartUM +import com.tangem.features.yield.supply.impl.chart.entity.YieldSupplyMarketChartData + +@Composable +internal fun YieldSupplyChartContent(state: YieldSupplyChartUM, modifier: Modifier = Modifier) { + val itemModifier = modifier + .height(105.dp) + .fillMaxWidth() + when (state) { + is YieldSupplyChartUM.Loading -> YieldSupplyChartLoading(modifier = itemModifier) + is YieldSupplyChartUM.Error -> { + Column( + modifier = itemModifier, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResourceSafe(R.string.markets_loading_error_title), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + SpacerH12() + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.try_to_load_data_again_button_title), + onClick = state.onRetry, + ), + ) + } + } + is YieldSupplyChartUM.Data -> YieldSupplyChartData(state = state, modifier = itemModifier) + } +} + +@Suppress("MagicNumber") +@Composable +private fun YieldSupplyChartLoading(modifier: Modifier = Modifier) { + Box( + modifier = modifier, + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 12.dp), + ) { + repeat(4) { index -> + TextShimmer( + modifier = Modifier + .size(16.dp), + radius = 4.dp, + style = TangemTheme.typography.body1, + ) + Spacer(modifier = Modifier.height(6.dp)) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 24.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + repeat(5) { + TextShimmer( + modifier = Modifier + .weight(1f) + .height(16.dp), + radius = 4.dp, + style = TangemTheme.typography.body1, + ) + } + } + } + + CircularProgressIndicator( + modifier = Modifier + .size(TangemTheme.dimens.size16) + .align(Alignment.Center), + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Composable +private fun YieldSupplyChartData(state: YieldSupplyChartUM.Data, modifier: Modifier = Modifier) { + val yValues = state.chartData.y + val model = CartesianChartModel(ColumnCartesianLayerModel.build { series(yValues) }) + + val layer = rememberColumnCartesianLayer() + + val startAxis: VerticalAxis = rememberCustomStartAxis( + label = rememberAxisLabelComponent(color = TangemTheme.colors.text.tertiary), + valueFormatter = CartesianValueFormatter { value, _, _ -> + val pct = value.toInt() + "$pct%" + }, + ) + + val bottomAxis: HorizontalAxis = rememberBottomAxis( + label = rememberAxisLabelComponent(color = TangemTheme.colors.text.tertiary), + guideline = null, + tick = null, + line = null, + ) + + val average = yValues.map { it.toDouble() }.average() + + val referenceLine = rememberHorizontalLine( + y = { average }, + line = rememberLineComponent( + color = TangemTheme.colors.text.tertiary, + thickness = TangemTheme.dimens.size1, + shape = Shape.dashed(Shape.Rectangle, TangemTheme.dimens.size4, TangemTheme.dimens.size4), + ), + ) + + val chart = rememberCartesianChart( + layer, + startAxis = startAxis, + bottomAxis = bottomAxis, + decorations = listOf(referenceLine), + horizontalLayout = HorizontalLayout.fullWidth(), + ) + + CartesianChartHost( + modifier = modifier, + chart = chart, + model = model, + zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), + scrollState = rememberVicoScrollState(scrollEnabled = false), + ) +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun YieldSupplyChartContent_Preview( + @PreviewParameter(YieldSupplyChartPreviewProvider::class) state: YieldSupplyChartUM, +) { + TangemThemePreview { + YieldSupplyChartContent(state = state) + } +} + +private class YieldSupplyChartPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + YieldSupplyChartUM.Loading, + YieldSupplyChartUM.Error(onRetry = {}), + YieldSupplyChartUM.Data(chartData = YieldSupplyMarketChartData.mock()), + ) +} +// endregion \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index c3db349280..3b196b4cfa 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -178,7 +178,9 @@ internal class YieldSupplyModel @Inject constructor( uiState.update { yieldSupplyUM } - (cryptoCurrency as? CryptoCurrency.Token)?.let(::loadTokenStatus) + if (yieldSupplyUM is YieldSupplyUM.Loading) { + (cryptoCurrency as? CryptoCurrency.Token)?.let(::loadTokenStatus) + } } private companion object {