Updated on 2026-08-14
This commit is contained in:
parent
d84c61dc58
commit
4c8c06416f
21 changed files with 620 additions and 91 deletions
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.features.yield.supply.impl.apy
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.yield.supply.impl.apy.ui.YieldSupplyApyContent
|
||||
import com.tangem.features.yield.supply.impl.chart.DefaultYieldSupplyChartComponent
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
internal class YieldSupplyApyComponent(
|
||||
private val appComponentContext: AppComponentContext,
|
||||
private val params: Params,
|
||||
) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
val loadingState: StateFlow<Boolean>
|
||||
field = MutableStateFlow<Boolean>(true)
|
||||
private val chartComponent = DefaultYieldSupplyChartComponent(
|
||||
appComponentContext = child("chartComponent"),
|
||||
params = DefaultYieldSupplyChartComponent.Params(
|
||||
cryptoCurrency = params.cryptoCurrency as CryptoCurrency.Token,
|
||||
callback = object : DefaultYieldSupplyChartComponent.ModelCallback {
|
||||
override fun onStartLoading() {
|
||||
loadingState.update { true }
|
||||
}
|
||||
|
||||
override fun onSuccessLoad() {
|
||||
loadingState.update { false }
|
||||
}
|
||||
|
||||
override fun onLoadFail() {
|
||||
loadingState.update { false }
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
override fun dismiss() {
|
||||
params.onBackClick()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val state by loadingState.collectAsState()
|
||||
YieldSupplyApyContent(
|
||||
isLoading = state,
|
||||
onBackClick = params.onBackClick,
|
||||
chartComponent = chartComponent,
|
||||
)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val onBackClick: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
package com.tangem.features.yield.supply.impl.apy.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.SpacerH2
|
||||
import com.tangem.core.ui.components.SpacerH24
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
import com.tangem.core.ui.components.SpacerW4
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
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.R
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun YieldSupplyApyContent(
|
||||
isLoading: Boolean,
|
||||
onBackClick: () -> Unit,
|
||||
chartComponent: ComposableContentComponent,
|
||||
) {
|
||||
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = onBackClick,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
onBack = onBackClick,
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
title = {
|
||||
TangemModalBottomSheetTitle(
|
||||
endIconRes = com.tangem.core.ui.R.drawable.ic_close_24,
|
||||
onEndClick = onBackClick,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.yield_module_rate_info_sheet_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
SpacerH8()
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.yield_module_rate_info_sheet_description),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
SpacerH12()
|
||||
val inlineIconId = "aaveIcon"
|
||||
Text(
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
text = buildAnnotatedString {
|
||||
append(stringResourceSafe(R.string.yield_module_rate_info_sheet_powered_by))
|
||||
append(" ")
|
||||
appendInlineContent(inlineIconId, "[icon]")
|
||||
append("Aave")
|
||||
},
|
||||
inlineContent = mapOf(
|
||||
inlineIconId to InlineTextContent(
|
||||
Placeholder(
|
||||
width = 16.sp,
|
||||
height = 16.sp,
|
||||
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
|
||||
),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.img_aave_22),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
SpacerH24()
|
||||
|
||||
ApyChart(
|
||||
isChartLoading = isLoading,
|
||||
chartComponent = chartComponent,
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
)
|
||||
|
||||
SpacerH16()
|
||||
|
||||
SecondaryButton(
|
||||
text = stringResourceSafe(R.string.common_got_it),
|
||||
onClick = onBackClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ApyChart(
|
||||
isChartLoading: Boolean,
|
||||
chartComponent: ComposableContentComponent,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
if (isChartLoading) {
|
||||
TextShimmer(
|
||||
text = stringResourceSafe(R.string.yield_module_historical_returns),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
SpacerH2()
|
||||
TextShimmer(
|
||||
text = stringResourceSafe(R.string.yield_module_supply_apr),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.yield_module_historical_returns),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
SpacerH2()
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
AccentDot()
|
||||
SpacerW4()
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.yield_module_supply_apr),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
SpacerH12()
|
||||
chartComponent.Content(Modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccentDot(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(4.dp)
|
||||
.size(TangemTheme.dimens.size8)
|
||||
.background(color = TangemTheme.colors.icon.accent, shape = CircleShape),
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun YieldSupplyApyContentContentPreview() {
|
||||
TangemThemePreview {
|
||||
YieldSupplyApyContent(
|
||||
isLoading = false,
|
||||
onBackClick = {},
|
||||
chartComponent = ComposableContentComponent.EMPTY,
|
||||
)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -6,6 +6,8 @@ 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.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.yield.supply.impl.chart.model.YieldSupplyChartModel
|
||||
import com.tangem.features.yield.supply.impl.chart.ui.YieldSupplyChartContent
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -14,13 +16,13 @@ import dagger.assisted.AssistedInject
|
|||
|
||||
internal class DefaultYieldSupplyChartComponent @AssistedInject constructor(
|
||||
@Assisted private val appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: YieldSupplyChartModel.Params,
|
||||
) : AppComponentContext by appComponentContext {
|
||||
@Assisted private val params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: YieldSupplyChartModel = getOrCreateModel(params = params)
|
||||
|
||||
@Composable
|
||||
fun Content(modifier: Modifier = Modifier) {
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
YieldSupplyChartContent(
|
||||
state = state,
|
||||
|
|
@ -30,6 +32,17 @@ internal class DefaultYieldSupplyChartComponent @AssistedInject constructor(
|
|||
|
||||
@AssistedFactory
|
||||
internal interface Factory {
|
||||
fun create(context: AppComponentContext, params: YieldSupplyChartModel.Params): DefaultYieldSupplyChartComponent
|
||||
fun create(context: AppComponentContext, params: Params): DefaultYieldSupplyChartComponent
|
||||
}
|
||||
|
||||
internal data class Params(
|
||||
val cryptoCurrency: CryptoCurrency.Token,
|
||||
val callback: ModelCallback? = null,
|
||||
)
|
||||
|
||||
internal interface ModelCallback {
|
||||
fun onStartLoading()
|
||||
fun onSuccessLoad()
|
||||
fun onLoadFail()
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,10 @@ internal sealed class YieldSupplyChartUM {
|
|||
data class Error(val onRetry: () -> Unit) : YieldSupplyChartUM()
|
||||
|
||||
@Immutable
|
||||
data class Data(val chartData: YieldSupplyMarketChartDataUM) : YieldSupplyChartUM()
|
||||
data class Data(
|
||||
val chartData: YieldSupplyMarketChartDataUM,
|
||||
val monthLables: ImmutableList<String>,
|
||||
) : YieldSupplyChartUM()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
|
@ -22,16 +25,18 @@ internal data class YieldSupplyMarketChartDataUM(
|
|||
val y: ImmutableList<Double>,
|
||||
val x: ImmutableList<Double>,
|
||||
val avr: Double,
|
||||
val percentFormat: String,
|
||||
) {
|
||||
companion object {
|
||||
@Suppress("MagicNumber")
|
||||
fun mock(): YieldSupplyMarketChartDataUM {
|
||||
// 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,
|
||||
4.6, 4.0, 4.2, 3.3, 2.5, 5.6, 4.2, 6.7, 3.5, 2.5, 4.5, 3.4,
|
||||
4.2, 6.7, 3.5, 2.5, 4.5, 3.4,
|
||||
).toImmutableList()
|
||||
val x = List(y.size) { 1.0 }.toImmutableList()
|
||||
return YieldSupplyMarketChartDataUM(y = y, x = x, avr = 5.15)
|
||||
return YieldSupplyMarketChartDataUM(y = y, x = x, avr = 5.15, "%.1f")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,34 +3,95 @@ 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.domain.yield.supply.models.YieldSupplyMarketChartData
|
||||
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetChartUseCase
|
||||
import com.tangem.features.yield.supply.impl.chart.DefaultYieldSupplyChartComponent
|
||||
import com.tangem.features.yield.supply.impl.chart.entity.YieldSupplyChartUM
|
||||
import com.tangem.features.yield.supply.impl.chart.entity.YieldSupplyMarketChartDataUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("UnusedPrivateProperty")
|
||||
@ModelScoped
|
||||
internal class YieldSupplyChartModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val yieldSupplyGetChartUseCase: YieldSupplyGetChartUseCase,
|
||||
) : Model() {
|
||||
|
||||
data class Params(
|
||||
val tokenContractAddress: String,
|
||||
val onRetry: () -> Unit = {},
|
||||
)
|
||||
|
||||
private val params: Params = paramsContainer.require()
|
||||
private val params: DefaultYieldSupplyChartComponent.Params = paramsContainer.require()
|
||||
|
||||
val uiState: StateFlow<YieldSupplyChartUM>
|
||||
field = MutableStateFlow<YieldSupplyChartUM>(YieldSupplyChartUM.Loading)
|
||||
|
||||
init {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
val chartData = YieldSupplyMarketChartDataUM.mock()
|
||||
uiState.update { YieldSupplyChartUM.Data(chartData = chartData) }
|
||||
loadChart()
|
||||
}
|
||||
|
||||
private fun loadChart() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
params.callback?.onStartLoading()
|
||||
yieldSupplyGetChartUseCase(params.cryptoCurrency).onRight { chartData ->
|
||||
if (chartData.y.isEmpty()) {
|
||||
uiState.update {
|
||||
YieldSupplyChartUM.Error({
|
||||
uiState.update { YieldSupplyChartUM.Loading }
|
||||
loadChart()
|
||||
})
|
||||
}
|
||||
params.callback?.onLoadFail()
|
||||
} else {
|
||||
uiState.update {
|
||||
YieldSupplyChartUM.Data(
|
||||
chartData = YieldSupplyMarketChartDataUM(
|
||||
y = chartData.y.toImmutableList(),
|
||||
x = chartData.x.toImmutableList(),
|
||||
avr = chartData.avr,
|
||||
percentFormat = getPercentFormatPattern(chartData),
|
||||
),
|
||||
monthLables = lastMonthLabels().toImmutableList(),
|
||||
)
|
||||
}
|
||||
params.callback?.onSuccessLoad()
|
||||
}
|
||||
}.onLeft {
|
||||
uiState.update {
|
||||
YieldSupplyChartUM.Error({
|
||||
uiState.update { YieldSupplyChartUM.Loading }
|
||||
loadChart()
|
||||
})
|
||||
}
|
||||
params.callback?.onLoadFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun getPercentFormatPattern(chartData: YieldSupplyMarketChartData): String {
|
||||
return when {
|
||||
chartData.y.all { it < 1.0 } -> "%.1f"
|
||||
chartData.y.all { it < 0.1 } -> "%.2f"
|
||||
else -> "%.0f"
|
||||
}
|
||||
}
|
||||
private fun lastMonthLabels(n: Int = MONTH_LABELS_COUNT, locale: Locale = Locale.getDefault()): List<String> {
|
||||
val calendar = Calendar.getInstance()
|
||||
val formatter = SimpleDateFormat("MMM", locale)
|
||||
|
||||
return (n downTo 1).map {
|
||||
calendar.add(Calendar.MONTH, -1)
|
||||
formatter.format(calendar.time)
|
||||
}.reversed()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MONTH_LABELS_COUNT = 5
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.yield.supply.impl.chart.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -11,25 +12,31 @@ 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.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.State
|
||||
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.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 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.layer.grouped
|
||||
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.cartesian.segmented
|
||||
import com.patrykandpatrick.vico.compose.common.component.rememberLineComponent
|
||||
import com.patrykandpatrick.vico.compose.common.shape.dashed
|
||||
import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout
|
||||
|
|
@ -40,6 +47,9 @@ 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.cartesian.decoration.HorizontalLine
|
||||
import com.patrykandpatrick.vico.core.cartesian.layer.ColumnCartesianLayer
|
||||
import com.patrykandpatrick.vico.core.cartesian.layer.ColumnCartesianLayer.MergeMode
|
||||
import com.patrykandpatrick.vico.core.common.shape.Shape
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
|
|
@ -52,6 +62,8 @@ 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.YieldSupplyMarketChartDataUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
internal fun YieldSupplyChartContent(state: YieldSupplyChartUM, modifier: Modifier = Modifier) {
|
||||
|
|
@ -122,64 +134,187 @@ private fun YieldSupplyChartLoading(modifier: Modifier = Modifier) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CircularProgressIndicator(
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun YieldSupplyChartData(state: YieldSupplyChartUM.Data, modifier: Modifier = Modifier) {
|
||||
val model = rememberChartModel(state.chartData)
|
||||
|
||||
val columnsLayer = rememberColumnsLayer(accentColor = TangemTheme.colors.text.accent)
|
||||
|
||||
val startAxis: VerticalAxis<AxisPosition.Vertical.Start> = rememberStartAxis(
|
||||
labelColor = TangemTheme.colors.text.tertiary,
|
||||
percentFormat = state.chartData.percentFormat,
|
||||
)
|
||||
|
||||
val bottomAxis: HorizontalAxis<AxisPosition.Horizontal.Bottom> = rememberBottomAxis(
|
||||
labelColor = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
||||
val referenceLineThickness = 2.dp
|
||||
val referenceLine = rememberReferenceLine(
|
||||
average = state.chartData.avr,
|
||||
color = TangemTheme.colors.icon.primary1,
|
||||
thickness = referenceLineThickness,
|
||||
)
|
||||
|
||||
val chart = rememberCartesianChart(
|
||||
columnsLayer,
|
||||
startAxis = startAxis,
|
||||
bottomAxis = bottomAxis,
|
||||
decorations = listOf(referenceLine),
|
||||
horizontalLayout = HorizontalLayout.segmented(),
|
||||
)
|
||||
|
||||
val averageLabelTopPadding by rememberAverageLabelTopPadding(state.chartData)
|
||||
|
||||
Box(modifier = modifier) {
|
||||
MonthLabelsRow(labels = state.monthLables, modifier = Modifier.align(Alignment.BottomEnd))
|
||||
CartesianChartHost(
|
||||
modifier = Modifier.padding(bottom = 4.dp),
|
||||
chart = chart,
|
||||
model = model,
|
||||
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
|
||||
scrollState = rememberVicoScrollState(scrollEnabled = false),
|
||||
)
|
||||
AverageLabel(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.align(Alignment.Center),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
.padding(start = 36.dp, top = averageLabelTopPadding)
|
||||
.height(24.dp),
|
||||
avr = state.chartData.avr.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun YieldSupplyChartData(state: YieldSupplyChartUM.Data, modifier: Modifier = Modifier) {
|
||||
val yValues = state.chartData.y
|
||||
val model = CartesianChartModel(ColumnCartesianLayerModel.build { series(yValues) })
|
||||
private fun rememberChartModel(data: YieldSupplyMarketChartDataUM): CartesianChartModel {
|
||||
return remember(data.y) {
|
||||
CartesianChartModel(
|
||||
ColumnCartesianLayerModel.build {
|
||||
series(data.y)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val layer = rememberColumnCartesianLayer()
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun rememberColumnsLayer(accentColor: Color): ColumnCartesianLayer {
|
||||
val column = rememberLineComponent(
|
||||
color = accentColor,
|
||||
thickness = 6.dp,
|
||||
shape = Shape.rounded(40),
|
||||
)
|
||||
return rememberColumnCartesianLayer(
|
||||
columnProvider = ColumnCartesianLayer.ColumnProvider.series(column),
|
||||
columnCollectionSpacing = 3.dp,
|
||||
mergeMode = { MergeMode.grouped() },
|
||||
)
|
||||
}
|
||||
|
||||
val startAxis: VerticalAxis<AxisPosition.Vertical.Start> = rememberCustomStartAxis(
|
||||
label = rememberAxisLabelComponent(color = TangemTheme.colors.text.tertiary),
|
||||
@Composable
|
||||
private fun rememberStartAxis(labelColor: Color, percentFormat: String): VerticalAxis<AxisPosition.Vertical.Start> {
|
||||
return rememberCustomStartAxis(
|
||||
label = rememberAxisLabelComponent(color = labelColor),
|
||||
valueFormatter = CartesianValueFormatter { value, _, _ ->
|
||||
val pct = value.toInt()
|
||||
val pct = String.format(Locale.getDefault(), percentFormat, value)
|
||||
"$pct%"
|
||||
},
|
||||
)
|
||||
|
||||
val bottomAxis: HorizontalAxis<AxisPosition.Horizontal.Bottom> = rememberBottomAxis(
|
||||
label = rememberAxisLabelComponent(color = TangemTheme.colors.text.tertiary),
|
||||
guideline = null,
|
||||
tick = null,
|
||||
line = null,
|
||||
)
|
||||
}
|
||||
|
||||
val average = yValues.map { it.toDouble() }.average()
|
||||
@Composable
|
||||
private fun rememberBottomAxis(labelColor: Color): HorizontalAxis<AxisPosition.Horizontal.Bottom> {
|
||||
return rememberBottomAxis(
|
||||
label = rememberAxisLabelComponent(color = labelColor),
|
||||
guideline = null,
|
||||
tick = null,
|
||||
line = null,
|
||||
valueFormatter = CartesianValueFormatter { _, _, _ ->
|
||||
""
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val referenceLine = rememberHorizontalLine(
|
||||
@Composable
|
||||
private fun rememberReferenceLine(average: Double, color: Color, thickness: Dp): HorizontalLine {
|
||||
return 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),
|
||||
color = color,
|
||||
thickness = thickness,
|
||||
shape = Shape.dashed(Shape.Pill, 4.dp, 2.dp),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val chart = rememberCartesianChart(
|
||||
layer,
|
||||
startAxis = startAxis,
|
||||
bottomAxis = bottomAxis,
|
||||
decorations = listOf(referenceLine),
|
||||
horizontalLayout = HorizontalLayout.fullWidth(),
|
||||
)
|
||||
@Composable
|
||||
private fun rememberAverageLabelTopPadding(data: YieldSupplyMarketChartDataUM): State<Dp> {
|
||||
return remember(data.y, data.avr) {
|
||||
derivedStateOf {
|
||||
val chartHeight = 105.dp
|
||||
val labelHeight = 24.dp
|
||||
val referenceLineThickness = 2.dp
|
||||
val desiredGap = 3.dp
|
||||
val opticalNudge = 1.dp
|
||||
val extraPaddingAboveLine = 2.dp
|
||||
val gapAboveLine = desiredGap + referenceLineThickness / 2 + opticalNudge + extraPaddingAboveLine
|
||||
val chartContentBottomInset = 10.dp
|
||||
|
||||
CartesianChartHost(
|
||||
modifier = modifier,
|
||||
chart = chart,
|
||||
model = model,
|
||||
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
|
||||
scrollState = rememberVicoScrollState(scrollEnabled = false),
|
||||
)
|
||||
val maxValueInSeries = (data.y.maxOrNull() ?: 0.0).coerceAtLeast(data.avr)
|
||||
val averageRatio = if (maxValueInSeries == 0.0) {
|
||||
0f
|
||||
} else {
|
||||
(data.avr / maxValueInSeries).coerceIn(0.0, 1.0).toFloat()
|
||||
}
|
||||
val computedTopPadding = (chartHeight - chartContentBottomInset) * (1f - averageRatio)
|
||||
-labelHeight - gapAboveLine
|
||||
if (computedTopPadding < 2.dp) 2.dp else computedTopPadding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MonthLabelsRow(labels: kotlinx.collections.immutable.ImmutableList<String>, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(start = 38.dp, top = 4.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
repeat(labels.size) { index ->
|
||||
Text(
|
||||
text = labels[index],
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(16.dp),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AverageLabel(avr: String, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier.background(
|
||||
color = TangemTheme.colors.icon.primary1,
|
||||
shape = TangemTheme.shapes.roundedCornersSmall2,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.yield_module_rate_info_sheet_chart_average, avr),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary2,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
|
|
@ -199,7 +334,10 @@ private class YieldSupplyChartPreviewProvider : PreviewParameterProvider<YieldSu
|
|||
get() = sequenceOf(
|
||||
YieldSupplyChartUM.Loading,
|
||||
YieldSupplyChartUM.Error(onRetry = {}),
|
||||
YieldSupplyChartUM.Data(chartData = YieldSupplyMarketChartDataUM.mock()),
|
||||
YieldSupplyChartUM.Data(
|
||||
chartData = YieldSupplyMarketChartDataUM.mock(),
|
||||
monthLables = persistentListOf("Jul", "Sep", "Nov", "Feb", "May"),
|
||||
),
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -11,8 +11,8 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.EmptyComposableBottomSheetComponent
|
||||
import com.tangem.features.yield.supply.api.YieldSupplyPromoComponent
|
||||
import com.tangem.features.yield.supply.impl.apy.YieldSupplyApyComponent
|
||||
import com.tangem.features.yield.supply.impl.promo.model.YieldSupplyPromoModel
|
||||
import com.tangem.features.yield.supply.impl.promo.ui.YieldSupplyPromoContent
|
||||
import com.tangem.features.yield.supply.impl.subcomponents.startearning.YieldSupplyStartEarningEntryComponent
|
||||
|
|
@ -52,7 +52,15 @@ internal class DefaultYieldSupplyPromoComponent @AssistedInject constructor(
|
|||
config: YieldSupplyPromoConfig,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent = when (config) {
|
||||
YieldSupplyPromoConfig.Apy -> EmptyComposableBottomSheetComponent
|
||||
YieldSupplyPromoConfig.Apy -> YieldSupplyApyComponent(
|
||||
appComponentContext = childByContext(componentContext),
|
||||
params = YieldSupplyApyComponent.Params(
|
||||
cryptoCurrency = params.currency,
|
||||
onBackClick = {
|
||||
model.bottomSheetNavigation.dismiss()
|
||||
},
|
||||
),
|
||||
)
|
||||
YieldSupplyPromoConfig.Action -> YieldSupplyStartEarningEntryComponent(
|
||||
appComponentContext = childByContext(componentContext),
|
||||
params = YieldSupplyStartEarningEntryComponent.Params(
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import com.tangem.features.yield.supply.impl.subcomponents.notifications.YieldSu
|
|||
import com.tangem.features.yield.supply.impl.subcomponents.notifications.YieldSupplyNotificationsUpdateTrigger
|
||||
import com.tangem.features.yield.supply.impl.subcomponents.notifications.entity.YieldSupplyNotificationData
|
||||
import com.tangem.utils.StringsSigns.DOT
|
||||
import com.tangem.utils.TangemLinks
|
||||
import com.tangem.utils.TangemBlogUrlBuilder
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -101,7 +101,7 @@ internal class YieldSupplyApproveModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun onReadMoreClick() {
|
||||
urlOpener.openUrl(TangemLinks.FEE_BLOG_LINK)
|
||||
urlOpener.openUrl(TangemBlogUrlBuilder.FEE_BLOG_LINK)
|
||||
}
|
||||
|
||||
override fun onFeeReload() {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import com.tangem.features.yield.supply.impl.subcomponents.notifications.YieldSu
|
|||
import com.tangem.features.yield.supply.impl.subcomponents.notifications.entity.YieldSupplyNotificationData
|
||||
import com.tangem.features.yield.supply.impl.subcomponents.stopearning.YieldSupplyStopEarningComponent
|
||||
import com.tangem.features.yield.supply.impl.subcomponents.stopearning.model.transformer.YieldSupplyStopEarningFeeContentTransformer
|
||||
import com.tangem.utils.TangemLinks
|
||||
import com.tangem.utils.TangemBlogUrlBuilder
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import com.tangem.utils.transformer.update
|
||||
|
|
@ -103,7 +103,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun onReadMoreClick() {
|
||||
urlOpener.openUrl(TangemLinks.FEE_BLOG_LINK)
|
||||
urlOpener.openUrl(TangemBlogUrlBuilder.FEE_BLOG_LINK)
|
||||
}
|
||||
|
||||
fun onClick() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue