Updated on 2026-08-14
This commit is contained in:
commit
2c8c2cb579
183 changed files with 3741 additions and 638 deletions
1
features/manage-tokens/.gitignore
vendored
Normal file
1
features/manage-tokens/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
1
features/manage-tokens/api/.gitignore
vendored
Normal file
1
features/manage-tokens/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
14
features/manage-tokens/api/build.gradle.kts
Normal file
14
features/manage-tokens/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.managetokens.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.features.managetokens.featuretoggles
|
||||
|
||||
interface ManageTokensFeatureToggles {
|
||||
val isRedesignedScreenEnabled: Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.features.managetokens.navigation
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
interface ManageTokensRouter {
|
||||
fun getEntryFragment(): Fragment
|
||||
}
|
||||
1
features/manage-tokens/impl/.gitignore
vendored
Normal file
1
features/manage-tokens/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
68
features/manage-tokens/impl/build.gradle.kts
Normal file
68
features/manage-tokens/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.managetokens.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.activity.compose)
|
||||
implementation(deps.material)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
implementation(deps.compose.coil)
|
||||
implementation(deps.compose.constraintLayout)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.material)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.navigation)
|
||||
implementation(deps.compose.navigation.hilt)
|
||||
implementation(deps.compose.paging)
|
||||
implementation(deps.compose.reorderable)
|
||||
implementation(deps.compose.shimmer)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
|
||||
/** Other libraries */
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.tangem.card.core)
|
||||
implementation(deps.tangem.blockchain)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.featuretoggles)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.common)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.demo)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.appCurrency)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
|
||||
/** Feature Apis */
|
||||
implementation(projects.features.manageTokens.api)
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.managetokens
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensRouter
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class ManageTokensFragment : ComposeFragment() {
|
||||
|
||||
@Inject
|
||||
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
@Inject
|
||||
lateinit var manageTokensRouter: ManageTokensRouter
|
||||
|
||||
// private val internalManageTokensRouter: InnerManageTokensRouter
|
||||
// get() = requireNotNull(manageTokensRouter as? InnerManageTokensRouter) {
|
||||
// "internalManageTokensRouter should be instance of InnerManageTokensRouter"
|
||||
// }
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
// val viewModel = hiltViewModel<ManageTokensViewModel>()
|
||||
// viewModel.router = [REDACTED_EMAIL]
|
||||
//
|
||||
// LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
|
||||
|
||||
val systemBarsColor = TangemTheme.colors.background.secondary
|
||||
SystemBarsEffect {
|
||||
setSystemBarsColor(systemBarsColor)
|
||||
}
|
||||
|
||||
// ManageTokensScreen(state = viewModel.uiState)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.managetokens.di
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles
|
||||
import com.tangem.managetokens.featuretoggles.DefaultManageTokensFeatureToggles
|
||||
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 ManageTokensFeatureTogglesModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWalletFeatureToggles(featureTogglesManager: FeatureTogglesManager): ManageTokensFeatureToggles {
|
||||
return DefaultManageTokensFeatureToggles(featureTogglesManager = featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.managetokens.di
|
||||
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensRouter
|
||||
import com.tangem.managetokens.presentation.router.DefaultManageTokensRouter
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityComponent
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
|
||||
@Module
|
||||
@InstallIn(ActivityComponent::class)
|
||||
internal object ManageTokensRouterModule {
|
||||
|
||||
@Provides
|
||||
@ActivityScoped
|
||||
fun provideManageTokensRouter(): ManageTokensRouter {
|
||||
return DefaultManageTokensRouter()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.managetokens.featuretoggles
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles
|
||||
|
||||
internal class DefaultManageTokensFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : ManageTokensFeatureToggles {
|
||||
override val isRedesignedScreenEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_MANAGE_TOKENS_SCREEN_ENABLED")
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state
|
||||
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.pluralReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
|
||||
data class DerivationNotificationState(
|
||||
val totalNeeded: Int,
|
||||
val missingAddressesCount: Int,
|
||||
val onGenerateClick: () -> Unit,
|
||||
) {
|
||||
val config = NotificationConfig(
|
||||
title = resourceReference(id = R.string.warning_missing_derivation_title),
|
||||
subtitle = pluralReference(
|
||||
id = R.plurals.warning_missing_derivation_message,
|
||||
count = totalNeeded,
|
||||
formatArgs = wrappedList(totalNeeded),
|
||||
),
|
||||
iconResId = R.drawable.ic_alert_circle_24,
|
||||
buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
|
||||
text = resourceReference(id = R.string.common_generate_addresses),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
onClick = onGenerateClick,
|
||||
additionalText = pluralReference(
|
||||
id = R.plurals.manage_tokens_number_of_wallets_android,
|
||||
count = totalNeeded,
|
||||
formatArgs = wrappedList(missingAddressesCount, totalNeeded),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal sealed class QuotesState {
|
||||
object Unknown : QuotesState()
|
||||
|
||||
data class Content(
|
||||
val priceChange: String,
|
||||
val changeType: PriceChangeType,
|
||||
val chartData: ImmutableList<Float>,
|
||||
) : QuotesState()
|
||||
}
|
||||
|
||||
enum class PriceChangeType {
|
||||
UP, DOWN
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state
|
||||
|
||||
internal enum class TokenButtonType {
|
||||
ADD, EDIT, NOT_AVAILABLE
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.extensions.ImageReference
|
||||
|
||||
internal data class TokenIconState(
|
||||
val iconReference: ImageReference?,
|
||||
val placeholderTint: Color,
|
||||
val placeholderBackground: Color,
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state
|
||||
|
||||
internal sealed class TokenItemState {
|
||||
|
||||
abstract val id: String
|
||||
|
||||
data class Loading(override val id: String) : TokenItemState()
|
||||
|
||||
data class Loaded(
|
||||
override val id: String,
|
||||
val name: String,
|
||||
val currencyId: String,
|
||||
val tokenIcon: TokenIconState,
|
||||
val quotes: QuotesState,
|
||||
val rate: String?,
|
||||
val availableAction: TokenButtonType,
|
||||
val onButtonClick: (String) -> Unit,
|
||||
) : TokenItemState()
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state.previewdata
|
||||
|
||||
import com.tangem.managetokens.presentation.managetokens.state.DerivationNotificationState
|
||||
|
||||
object DerivationNotificationStatePreviewData {
|
||||
val state = DerivationNotificationState(
|
||||
totalNeeded = 3,
|
||||
missingAddressesCount = 2,
|
||||
onGenerateClick = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state.previewdata
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.managetokens.presentation.managetokens.state.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal object TokenItemStatePreviewData {
|
||||
|
||||
val tokenLoading: TokenItemState
|
||||
get() = TokenItemState.Loading("id")
|
||||
|
||||
val loadedPriceDown: TokenItemState
|
||||
get() = TokenItemState.Loaded(
|
||||
id = "BTC",
|
||||
name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin",
|
||||
currencyId = "BTC",
|
||||
tokenIcon = tokenIconState,
|
||||
quotes = QuotesState.Content(
|
||||
priceChange = "0.43%",
|
||||
changeType = PriceChangeType.DOWN,
|
||||
chartData = persistentListOf(10f, 2f, 5f, 3f, 4f, 8f, 9f, 7f, 4f),
|
||||
),
|
||||
rate = "31 285.72$",
|
||||
availableAction = TokenButtonType.ADD,
|
||||
onButtonClick = {},
|
||||
)
|
||||
|
||||
val loadedPriceUp: TokenItemState
|
||||
get() = TokenItemState.Loaded(
|
||||
id = "BTC",
|
||||
name = "Bitcoin",
|
||||
currencyId = "BTC",
|
||||
tokenIcon = tokenIconState,
|
||||
quotes = QuotesState.Content(
|
||||
priceChange = "0.43%",
|
||||
changeType = PriceChangeType.UP,
|
||||
chartData = persistentListOf(1f, 3f, 4f, 8f, 12f, 10f, 8f, 3f, 5f, 7f),
|
||||
),
|
||||
rate = "31 285.72$",
|
||||
availableAction = TokenButtonType.NOT_AVAILABLE,
|
||||
onButtonClick = {},
|
||||
)
|
||||
|
||||
private val tokenIconState: TokenIconState
|
||||
get() = TokenIconState(
|
||||
iconReference = null,
|
||||
placeholderTint = Color.White,
|
||||
placeholderBackground = Color.Black,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconEndTwoLines
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.managetokens.state.previewdata.DerivationNotificationStatePreviewData
|
||||
|
||||
@Composable
|
||||
internal fun DerivationNotification(config: NotificationConfig, modifier: Modifier = Modifier) {
|
||||
BaseContainer(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
|
||||
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
MainContent(
|
||||
iconResId = config.iconResId,
|
||||
iconTint = TangemTheme.colors.icon.accent,
|
||||
title = config.title,
|
||||
subtitle = config.subtitle,
|
||||
)
|
||||
val buttonConfig = config.buttonsState
|
||||
if (buttonConfig is NotificationConfig.ButtonsState.PrimaryButtonConfig) {
|
||||
PrimaryButtonIconEndTwoLines(
|
||||
text = buttonConfig.text.resolveReference(),
|
||||
iconResId = buttonConfig.iconResId ?: R.drawable.ic_tangem_24,
|
||||
onClick = buttonConfig.onClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
additionalText = buttonConfig.additionalText?.resolveReference(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
.defaultMinSize(minHeight = TangemTheme.dimens.size62)
|
||||
.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(
|
||||
topStart = TangemTheme.dimens.radius16,
|
||||
topEnd = TangemTheme.dimens.radius16,
|
||||
),
|
||||
elevation = TangemTheme.dimens.elevation12,
|
||||
backgroundColor = TangemTheme.colors.background.action,
|
||||
) {
|
||||
Box(content = content)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainContent(iconResId: Int, iconTint: Color, title: TextReference, subtitle: TextReference) {
|
||||
Row {
|
||||
NotificationIcon(iconResId = iconResId, iconTint = iconTint)
|
||||
SpacerW(width = TangemTheme.dimens.spacing10)
|
||||
TextsBlock(title = title, subtitle = subtitle)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.NotificationIcon(iconResId: Int, iconTint: Color) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.CenterVertically),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size36)
|
||||
.background(
|
||||
color = iconTint.copy(alpha = 0.12f),
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = iconResId),
|
||||
contentDescription = null,
|
||||
tint = iconTint,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TextsBlock(title: TextReference, subtitle: TextReference) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2)) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.button,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = subtitle.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_ManageTokensScreen_LightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
DerivationNotification(DerivationNotificationStatePreviewData.state.config)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_ManageTokensScreen_DarkTheme() {
|
||||
TangemTheme(isDark = true) {
|
||||
DerivationNotification(DerivationNotificationStatePreviewData.state.config)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.ui.components
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* A chart with a solid line and gradient underneath. It can accept values of any range and number.
|
||||
* If the last value is bigger or equal to the first, the chart is of accent color, otherwise it's warning color.
|
||||
*
|
||||
* @param values a list of float values for a chart.
|
||||
**/
|
||||
@Composable
|
||||
fun PriceChangesChart(values: ImmutableList<Float>, modifier: Modifier = Modifier) {
|
||||
Row(modifier = modifier) {
|
||||
if (values.size < 2) return // escape without drawing when there are not enough points
|
||||
|
||||
val lineColor = if (values.last() >= values.first()) {
|
||||
TangemTheme.colors.icon.accent
|
||||
} else {
|
||||
TangemTheme.colors.icon.warning
|
||||
}
|
||||
val gradient = Brush.verticalGradient(
|
||||
colors = listOf(lineColor.copy(alpha = 0.21f), lineColor.copy(alpha = 0.0f)),
|
||||
)
|
||||
Chart(list = values, lineColor = lineColor, gradient = gradient, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Chart(list: ImmutableList<Float>, lineColor: Color, gradient: Brush, modifier: Modifier = Modifier) {
|
||||
val max = list.max()
|
||||
val min = list.min()
|
||||
val zipList: List<Pair<Float, Float>> = list.zipWithNext()
|
||||
|
||||
for (pair in zipList) {
|
||||
val fromValuePercentage = getValuePercentageForRange(pair.first, max, min)
|
||||
val toValuePercentage = getValuePercentageForRange(pair.second, max, min)
|
||||
|
||||
Canvas(
|
||||
modifier = modifier.fillMaxHeight(),
|
||||
onDraw = {
|
||||
val fromPoint = Offset(x = 0f, y = size.height.times(1 - fromValuePercentage))
|
||||
val toPoint = Offset(x = size.width, y = size.height.times(1 - toValuePercentage))
|
||||
|
||||
val path = drawChartLineAndCreatePath(fromPoint = fromPoint, toPoint = toPoint, lineColor = lineColor)
|
||||
|
||||
fillChart(
|
||||
path = path,
|
||||
fromPoint = fromPoint,
|
||||
toPoint = toPoint,
|
||||
size = size,
|
||||
gradient = gradient,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawChartLineAndCreatePath(fromPoint: Offset, toPoint: Offset, lineColor: Color): Path {
|
||||
val path = Path()
|
||||
path.moveTo(fromPoint.x, fromPoint.y)
|
||||
path.lineTo(toPoint.x, toPoint.y)
|
||||
drawPath(
|
||||
path = path,
|
||||
color = lineColor,
|
||||
style = Stroke(width = 1f),
|
||||
)
|
||||
return path
|
||||
}
|
||||
|
||||
private fun DrawScope.fillChart(path: Path, fromPoint: Offset, toPoint: Offset, size: Size, gradient: Brush) {
|
||||
path.lineTo(toPoint.x, size.height)
|
||||
path.lineTo(fromPoint.x, size.height)
|
||||
path.lineTo(0f, fromPoint.y)
|
||||
drawPath(
|
||||
path = path,
|
||||
brush = gradient,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getValuePercentageForRange(value: Float, max: Float, min: Float): Float {
|
||||
return if (max == min) { // to draw a straight line when all values are the same
|
||||
val modifiedMax = max + 1
|
||||
val modifiedMin = min - 1
|
||||
(value - modifiedMin) / (modifiedMax - modifiedMin)
|
||||
} else {
|
||||
(value - min) / (max - min)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 150, heightDp = 150, showBackground = true)
|
||||
@Composable
|
||||
private fun Chart_Positive_Preview() {
|
||||
TangemTheme(isDark = true) {
|
||||
PriceChangesChart(
|
||||
persistentListOf(1f, 2f, 4f, 1f, 5f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 150, heightDp = 150, showBackground = true)
|
||||
@Composable
|
||||
private fun Chart_Negative_Preview() {
|
||||
TangemTheme(isDark = true) {
|
||||
PriceChangesChart(
|
||||
persistentListOf(10f, 2f, 4f, 1f, 5f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.core.ui.components.buttons.PrimarySmallButton
|
||||
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.res.TangemTheme
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.managetokens.state.TokenButtonType
|
||||
|
||||
@Composable
|
||||
internal fun TokenButton(type: TokenButtonType, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
when (type) {
|
||||
TokenButtonType.ADD -> PrimarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = resourceReference(R.string.manage_tokens_add),
|
||||
onClick = onClick,
|
||||
),
|
||||
modifier = modifier,
|
||||
)
|
||||
TokenButtonType.EDIT -> SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = resourceReference(R.string.manage_tokens_edit),
|
||||
onClick = onClick,
|
||||
),
|
||||
modifier = modifier,
|
||||
)
|
||||
TokenButtonType.NOT_AVAILABLE -> {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(height = TangemTheme.dimens.size24, width = TangemTheme.dimens.size46)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(
|
||||
bounded = false,
|
||||
radius = TangemTheme.dimens.size20,
|
||||
),
|
||||
onClick = onClick,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size20),
|
||||
painter = painterResource(id = R.drawable.ic_information_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(backgroundColor = 0xffffff, showBackground = true)
|
||||
@Composable
|
||||
private fun TokenButton_Light_Preview(@PreviewParameter(TokenButtonTypeProvider::class) type: TokenButtonType) {
|
||||
TangemTheme(isDark = false) {
|
||||
TokenButton(type = type, {})
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun TokenButton_Dark_Preview(@PreviewParameter(TokenButtonTypeProvider::class) type: TokenButtonType) {
|
||||
TangemTheme(isDark = true) {
|
||||
TokenButton(type = type, {})
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenButtonTypeProvider : PreviewParameterProvider<TokenButtonType> {
|
||||
override val values = sequenceOf(
|
||||
TokenButtonType.ADD,
|
||||
TokenButtonType.EDIT,
|
||||
TokenButtonType.NOT_AVAILABLE,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
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.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.extensions.ImageReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.managetokens.state.TokenIconState
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
internal fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier) {
|
||||
val iconModifier = modifier
|
||||
.size(TangemTheme.dimens.size36)
|
||||
if (state.iconReference != null) {
|
||||
DefaultCurrencyIcon(
|
||||
modifier = iconModifier,
|
||||
iconReference = state.iconReference,
|
||||
errorIcon = {
|
||||
PlaceholderIcon(
|
||||
modifier = iconModifier,
|
||||
tint = state.placeholderTint,
|
||||
background = state.placeholderBackground,
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
PlaceholderIcon(
|
||||
modifier = iconModifier,
|
||||
tint = state.placeholderTint,
|
||||
background = state.placeholderBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PlaceholderIcon(tint: Color, background: Color, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = background,
|
||||
shape = CircleShape,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.matchParentSize(),
|
||||
painter = painterResource(id = R.drawable.ic_custom_token_44),
|
||||
tint = tint,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private inline fun DefaultCurrencyIcon(
|
||||
iconReference: ImageReference,
|
||||
crossinline errorIcon: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
|
||||
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
SubcomposeAsyncImage(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = iconBackgroundColor,
|
||||
shape = TangemTheme.shapes.roundedCorners8,
|
||||
),
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(iconReference.getReference())
|
||||
.crossfade(enable = true)
|
||||
.allowHardware(false)
|
||||
.listener(
|
||||
onSuccess = { _, result ->
|
||||
if (isDarkTheme) {
|
||||
coroutineScope.launch {
|
||||
val color = ImageBackgroundContrastChecker(
|
||||
drawable = result.drawable,
|
||||
backgroundColor = itemBackgroundColor,
|
||||
).getContrastColorIfNeeded(isDarkTheme)
|
||||
iconBackgroundColor = color
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.build(),
|
||||
loading = { LoadingIcon() },
|
||||
error = { errorIcon() },
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun LoadingIcon(modifier: Modifier = Modifier) {
|
||||
CircleShimmer(modifier = modifier)
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.tangem.core.ui.components.SpacerW4
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.managetokens.state.PriceChangeType
|
||||
import com.tangem.managetokens.presentation.managetokens.state.QuotesState
|
||||
|
||||
@Composable
|
||||
internal fun TokenPriceChange(state: QuotesState, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is QuotesState.Content ->
|
||||
PriceChangeBlock(modifier = modifier, type = state.changeType, text = state.priceChange)
|
||||
QuotesState.Unknown -> PriceChangeBlock(modifier = modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PriceChangeBlock(modifier: Modifier = Modifier, type: PriceChangeType? = null, text: String? = null) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
PriceChangeIcon(type = type)
|
||||
SpacerW4()
|
||||
PriceChangeText(type = type, text = text)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PriceChangeIcon(type: PriceChangeType?) {
|
||||
AnimatedContent(targetState = type, label = "Update the price change's arrow") { animatedType ->
|
||||
animatedType ?: return@AnimatedContent
|
||||
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
id = when (animatedType) {
|
||||
PriceChangeType.UP -> R.drawable.ic_arrow_up_8
|
||||
PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8
|
||||
},
|
||||
),
|
||||
tint = when (animatedType) {
|
||||
PriceChangeType.UP -> TangemTheme.colors.icon.accent
|
||||
PriceChangeType.DOWN -> TangemTheme.colors.icon.warning
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PriceChangeText(type: PriceChangeType?, text: String?) {
|
||||
AnimatedContent(targetState = text, label = "Update the price change's text") { animatedText ->
|
||||
animatedText ?: return@AnimatedContent
|
||||
|
||||
Text(
|
||||
text = animatedText,
|
||||
color = when (type) {
|
||||
PriceChangeType.UP -> TangemTheme.colors.text.accent
|
||||
PriceChangeType.DOWN -> TangemTheme.colors.text.warning
|
||||
null -> TangemTheme.colors.text.primary1
|
||||
},
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.managetokens.presentation.managetokens.state.QuotesState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData
|
||||
|
||||
private val TOKEN_ITEM_HEIGHT: Dp
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
get() = TangemTheme.dimens.size68
|
||||
|
||||
@Composable
|
||||
internal fun TokenRowItem(state: TokenItemState, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is TokenItemState.Loading -> LoadingTokenItem(modifier)
|
||||
is TokenItemState.Loaded -> LoadedTokenItem(state, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadedTokenItem(state: TokenItemState.Loaded, modifier: Modifier = Modifier) {
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = TOKEN_ITEM_HEIGHT)
|
||||
.background(TangemTheme.colors.background.primary),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
val width = maxWidth
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
TokenIcon(state = state.tokenIcon)
|
||||
SpacerW12()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(weight = 1f),
|
||||
) {
|
||||
TokenName(name = state.name, currencyId = state.currencyId)
|
||||
TokenPriceData(price = state.rate, quotesState = state.quotes)
|
||||
}
|
||||
SpacerW24()
|
||||
|
||||
if (width > TangemTheme.dimens.size350 && // hide chart for small screens
|
||||
state.quotes is QuotesState.Content
|
||||
) {
|
||||
Chart(quotes = state.quotes)
|
||||
SpacerW24()
|
||||
}
|
||||
|
||||
TokenButton(
|
||||
type = state.availableAction,
|
||||
onClick = { state.onButtonClick(state.currencyId) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenName(name: String, currencyId: String) {
|
||||
Row {
|
||||
Text(
|
||||
text = name,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.weight(weight = 1f, fill = false),
|
||||
)
|
||||
SpacerW(width = TangemTheme.dimens.spacing6)
|
||||
Text(
|
||||
text = currencyId,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenPriceData(price: String?, quotesState: QuotesState) {
|
||||
if (price != null) {
|
||||
Row {
|
||||
Text(
|
||||
text = price,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.weight(weight = 1f, fill = false),
|
||||
)
|
||||
SpacerW(width = TangemTheme.dimens.spacing6)
|
||||
TokenPriceChange(state = quotesState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Chart(quotes: QuotesState.Content) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(width = TangemTheme.dimens.size50, height = TangemTheme.dimens.size28),
|
||||
) {
|
||||
PriceChangesChart(values = quotes.chartData)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingTokenItem(modifier: Modifier = Modifier) {
|
||||
BaseSurface(modifier) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing4,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size36))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10)) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(
|
||||
width = TangemTheme.dimens.size70,
|
||||
height = TangemTheme.dimens.size12,
|
||||
),
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(
|
||||
width = TangemTheme.dimens.size52,
|
||||
height = TangemTheme.dimens.size12,
|
||||
),
|
||||
)
|
||||
}
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(
|
||||
width = TangemTheme.dimens.size46,
|
||||
height = TangemTheme.dimens.size12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BaseSurface(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
|
||||
Surface(
|
||||
modifier = modifier.defaultMinSize(minHeight = TOKEN_ITEM_HEIGHT),
|
||||
color = TangemTheme.colors.background.primary,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview()
|
||||
@Composable
|
||||
private fun Preview_Tokens_LightTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) {
|
||||
TangemTheme(isDark = false) {
|
||||
TokenRowItem(state)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_Tokens_DarkTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) {
|
||||
TangemTheme(isDark = true) {
|
||||
TokenRowItem(state)
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenConfigProvider : CollectionPreviewParameterProvider<TokenItemState>(
|
||||
collection = listOf(
|
||||
TokenItemStatePreviewData.tokenLoading,
|
||||
TokenItemStatePreviewData.loadedPriceDown,
|
||||
TokenItemStatePreviewData.loadedPriceUp,
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.managetokens.presentation.router
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensRouter
|
||||
import com.tangem.managetokens.ManageTokensFragment
|
||||
|
||||
internal class DefaultManageTokensRouter : ManageTokensRouter {
|
||||
override fun getEntryFragment(): Fragment = ManageTokensFragment()
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.send.api.featuretoggles
|
||||
|
||||
/**
|
||||
* Send feature toggles
|
||||
*/
|
||||
interface SendFeatureToggles {
|
||||
|
||||
/** Availability of redesigned send screen */
|
||||
val isRedesignedSendEnabled: Boolean
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.send.navigation
|
||||
package com.tangem.features.send.api.navigation
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
|
|
@ -8,5 +8,6 @@ interface SendRouter {
|
|||
|
||||
companion object {
|
||||
const val CRYPTO_CURRENCY_KEY = "send_crypto_currency"
|
||||
const val USER_WALLET_ID_KEY = "send_user_wallet_id"
|
||||
}
|
||||
}
|
||||
1
features/send/impl/.gitignore
vendored
Normal file
1
features/send/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
56
features/send/impl/build.gradle.kts
Normal file
56
features/send/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.send.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
implementation(deps.androidx.appCompat)
|
||||
|
||||
/** Other dependencies */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.material)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.tangem.card.core)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.navigation)
|
||||
implementation(deps.compose.navigation.hilt)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.featuretoggles)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.appCurrency)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
|
||||
/** Feature modules */
|
||||
implementation(projects.features.send.api)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.features.send.impl.di
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
|
||||
import com.tangem.features.send.impl.featuretoggles.DefaultSendFeatureToggles
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* DI module provides implementation of [SendFeatureToggles]
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object SendFeatureTogglesModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSendFeatureToggles(featureTogglesManager: FeatureTogglesManager): SendFeatureToggles {
|
||||
return DefaultSendFeatureToggles(featureTogglesManager = featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.features.send.impl.di
|
||||
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.send.impl.navigation.DefaultSendRouter
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityComponent
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
|
||||
/**
|
||||
* DI module provides implementation of [SendRouter]
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(ActivityComponent::class)
|
||||
internal object SendRouterModule {
|
||||
|
||||
@Provides
|
||||
@ActivityScoped
|
||||
fun provideSendRouter(): SendRouter {
|
||||
return DefaultSendRouter()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.features.send.impl.featuretoggles
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
|
||||
|
||||
/**
|
||||
* Default implementation of Send feature toggles
|
||||
*
|
||||
* @property featureTogglesManager manager for getting information about the availability of feature toggles
|
||||
*/
|
||||
internal class DefaultSendFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : SendFeatureToggles {
|
||||
override val isRedesignedSendEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_SEND_SCREEN_ENABLED")
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.send.impl.navigation
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.send.impl.presentation.SendFragment
|
||||
|
||||
internal class DefaultSendRouter : SendRouter {
|
||||
override fun getEntryFragment(): Fragment = SendFragment.create()
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.tangem.features.send.impl.presentation
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
import com.tangem.core.ui.screen.ComposeBottomSheetFragment
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import com.tangem.features.send.impl.presentation.send.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.send.ui.SendScreen
|
||||
import com.tangem.features.send.impl.presentation.send.viewmodel.SendViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Send fragment
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
internal class SendFragment : ComposeBottomSheetFragment() {
|
||||
|
||||
@Inject
|
||||
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
override val expandedHeightFraction: Float = 1f
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
val viewModel = hiltViewModel<SendViewModel>()
|
||||
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
|
||||
|
||||
SystemBarsEffect { setSystemBarsColor(color = Color.Transparent) }
|
||||
BackHandler { dismiss() }
|
||||
|
||||
when (val state = viewModel.uiState) {
|
||||
is SendUiState.Content -> SendScreen(state)
|
||||
SendUiState.Dismiss -> dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/** Create send fragment instance */
|
||||
fun create(): SendFragment = SendFragment()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.features.send.impl.presentation.send.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
/**
|
||||
* Segmented buttons config
|
||||
*
|
||||
* @param title button title
|
||||
* @param iconState currency icon state
|
||||
* @param iconUrl currency icon url
|
||||
* @param isFiat is fiat currency
|
||||
*/
|
||||
@Immutable
|
||||
internal data class SendAmountSegmentedButtonsConfig(
|
||||
val title: TextReference,
|
||||
val iconState: TokenIconState? = null,
|
||||
val iconUrl: String? = null,
|
||||
val isFiat: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.tangem.features.send.impl.presentation.send.state
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldConverter
|
||||
import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class SendAmountStateConverter(
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val userWalletProvider: Provider<UserWallet?>,
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
|
||||
private val sendAmountFieldConverter: SendAmountFieldConverter,
|
||||
) : Converter<Either<CurrencyStatusError, CryptoCurrencyStatus>, SendUiState> {
|
||||
|
||||
override fun convert(value: Either<CurrencyStatusError, CryptoCurrencyStatus>): SendUiState {
|
||||
val userWallet = userWalletProvider() ?: return currentStateProvider()
|
||||
val appCurrency = appCurrencyProvider()
|
||||
return value.fold(
|
||||
ifLeft = {
|
||||
// TODO add error handling
|
||||
currentStateProvider()
|
||||
},
|
||||
ifRight = {
|
||||
val fiat = formatFiatAmount(it.value.fiatAmount, appCurrency.code, appCurrency.symbol)
|
||||
val crypto = formatCryptoAmount(it.value.amount, it.currency.symbol, it.currency.decimals)
|
||||
SendUiState.Content.AmountState(
|
||||
cryptoCurrencyStatus = it,
|
||||
walletName = userWallet.name,
|
||||
walletBalance = "$crypto ($fiat)",
|
||||
tokenIconState = iconStateConverter.convert(it),
|
||||
appCurrency = appCurrency,
|
||||
amountTextField = sendAmountFieldConverter.convert(Unit),
|
||||
isFiatValue = false,
|
||||
clickIntents = clickIntents,
|
||||
segmentedButtonConfig = persistentListOf(
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(it.currency.symbol),
|
||||
iconState = iconStateConverter.convert(it),
|
||||
isFiat = false,
|
||||
),
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(appCurrency.code),
|
||||
iconState = iconStateConverter.convert(it),
|
||||
isFiat = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.tangem.features.send.impl.presentation.send.state
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldChangeConverter
|
||||
import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldConverter
|
||||
import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents
|
||||
|
||||
internal class SendStateFactory(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val userWalletProvider: Provider<UserWallet?>,
|
||||
) {
|
||||
|
||||
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
|
||||
private val amountFieldConverter by lazy { SendAmountFieldConverter(clickIntents) }
|
||||
|
||||
private val amountFieldChangeConverter by lazy { SendAmountFieldChangeConverter(currentStateProvider) }
|
||||
|
||||
private val amountStateConverter by lazy {
|
||||
SendAmountStateConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
clickIntents = clickIntents,
|
||||
iconStateConverter = iconStateConverter,
|
||||
userWalletProvider = userWalletProvider,
|
||||
sendAmountFieldConverter = amountFieldConverter,
|
||||
)
|
||||
}
|
||||
|
||||
fun getInitialState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents)
|
||||
|
||||
fun getAmountState(cryptoCurrencyStatus: Either<CurrencyStatusError, CryptoCurrencyStatus>): SendUiState {
|
||||
return amountStateConverter.convert(cryptoCurrencyStatus)
|
||||
}
|
||||
|
||||
fun getOnReceiveState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents)
|
||||
|
||||
fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value)
|
||||
|
||||
fun getOnCurrencyChangedState(isFiat: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val amountState = state as? SendUiState.Content.AmountState ?: return state
|
||||
|
||||
return if (amountState.isFiatValue == isFiat) {
|
||||
state
|
||||
} else {
|
||||
return state.copy(isFiatValue = isFiat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.features.send.impl.presentation.send.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.send.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
||||
/**
|
||||
* Ui states of the send screen
|
||||
*/
|
||||
@Immutable
|
||||
internal sealed class SendUiState {
|
||||
|
||||
/** States with content */
|
||||
sealed class Content : SendUiState() {
|
||||
|
||||
/** Is primary button enabled */
|
||||
abstract val isPrimaryButtonEnabled: Boolean
|
||||
|
||||
/** Click intents */
|
||||
abstract val clickIntents: SendClickIntents
|
||||
|
||||
/** Initial state */
|
||||
data class Initial(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
override val clickIntents: SendClickIntents,
|
||||
) : Content()
|
||||
|
||||
/** Amount state */
|
||||
data class AmountState(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
override val clickIntents: SendClickIntents,
|
||||
val walletName: String,
|
||||
val walletBalance: String,
|
||||
val tokenIconState: TokenIconState,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val appCurrency: AppCurrency,
|
||||
val isFiatValue: Boolean,
|
||||
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
|
||||
val amountTextField: SendTextField.Amount,
|
||||
) : Content()
|
||||
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Recipient state */
|
||||
data class RecipientState(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
override val clickIntents: SendClickIntents,
|
||||
) : Content()
|
||||
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Fee and speed state */
|
||||
data class FeeState(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
override val clickIntents: SendClickIntents,
|
||||
) : Content()
|
||||
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Send state */
|
||||
data class SendState(
|
||||
override val isPrimaryButtonEnabled: Boolean = true,
|
||||
override val clickIntents: SendClickIntents,
|
||||
) : Content()
|
||||
}
|
||||
|
||||
/** Dismiss screen */
|
||||
object Dismiss : SendUiState()
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.tangem.features.send.impl.presentation.send.state.fields
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.features.send.impl.presentation.send.state.SendUiState
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.text.NumberFormat
|
||||
|
||||
internal class SendAmountFieldChangeConverter(
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
) : Converter<String, SendUiState> {
|
||||
override fun convert(value: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
|
||||
if (
|
||||
state !is SendUiState.Content.AmountState ||
|
||||
value.checkDecimalSeparatorDuplicate()
|
||||
) {
|
||||
return state
|
||||
}
|
||||
|
||||
if (value.isEmpty()) return state.emptyState()
|
||||
|
||||
val fiatRate = state.cryptoCurrencyStatus.value.fiatRate
|
||||
|
||||
val trimmedValue = value.trim()
|
||||
|
||||
val cryptoValue = if (state.isFiatValue) {
|
||||
if (value.isNotBlank()) {
|
||||
trimmedValue.toBigDecimal().divide(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty()
|
||||
} else {
|
||||
DEFAULT_VALUE
|
||||
}
|
||||
} else {
|
||||
trimmedValue
|
||||
}
|
||||
|
||||
val fiatValue = if (!state.isFiatValue) {
|
||||
if (value.isNotBlank()) {
|
||||
trimmedValue.toBigDecimal().multiply(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty()
|
||||
} else {
|
||||
DEFAULT_VALUE
|
||||
}
|
||||
} else {
|
||||
trimmedValue
|
||||
}
|
||||
|
||||
val isExceedBalance = value.checkExceedBalance(state)
|
||||
return state.copy(
|
||||
amountTextField = state.amountTextField.copy(
|
||||
value = cryptoValue,
|
||||
fiatValue = fiatValue,
|
||||
isError = isExceedBalance,
|
||||
),
|
||||
isPrimaryButtonEnabled = !isExceedBalance,
|
||||
)
|
||||
}
|
||||
|
||||
private fun SendUiState.Content.AmountState.emptyState(): SendUiState {
|
||||
return copy(
|
||||
amountTextField = amountTextField.copy(
|
||||
value = if (!isFiatValue) "" else DEFAULT_VALUE,
|
||||
fiatValue = if (isFiatValue) "" else DEFAULT_VALUE,
|
||||
isError = false,
|
||||
),
|
||||
isPrimaryButtonEnabled = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.checkDecimalSeparatorDuplicate(): Boolean {
|
||||
val regex = "[\\.\\,]".toRegex()
|
||||
val decimalSeparatorCount = regex.findAll(this).count()
|
||||
|
||||
return decimalSeparatorCount > 1
|
||||
}
|
||||
|
||||
private fun String.checkExceedBalance(state: SendUiState.Content.AmountState): Boolean {
|
||||
val currencyStatus = state.cryptoCurrencyStatus.value
|
||||
return if (state.isFiatValue) {
|
||||
toBigDecimal() > currencyStatus.fiatAmount
|
||||
} else {
|
||||
toBigDecimal() > currencyStatus.amount
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.trim(): String {
|
||||
var trimmedValue = this
|
||||
if (length > 1 && firstOrNull() == '0' && get(1).isDigit()) trimmedValue = drop(1)
|
||||
|
||||
val separatorChar = DecimalFormatSymbols.getInstance().decimalSeparator.toString()
|
||||
return trimmedValue.replace("[\\.\\,]".toRegex(), separatorChar)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.features.send.impl.presentation.send.state.fields
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.text.NumberFormat
|
||||
|
||||
internal class SendAmountFieldConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
) : Converter<Unit, SendTextField.Amount> {
|
||||
|
||||
override fun convert(value: Unit): SendTextField.Amount {
|
||||
return SendTextField.Amount(
|
||||
value = "",
|
||||
fiatValue = DEFAULT_VALUE,
|
||||
onValueChange = clickIntents::onAmountValueChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
label = TextReference.Str(""),
|
||||
placeholder = TextReference.Str(DEFAULT_VALUE),
|
||||
isError = false,
|
||||
error = TextReference.Res(R.string.send_insufficient_funds),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.features.send.impl.presentation.send.state.fields
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal sealed class SendTextField {
|
||||
|
||||
/** Current value */
|
||||
abstract val value: String
|
||||
|
||||
/** Lambda be invoked when value is been changed */
|
||||
abstract val onValueChange: (String) -> Unit
|
||||
|
||||
/** Keyboard options */
|
||||
abstract val keyboardOptions: KeyboardOptions
|
||||
|
||||
/** Label */
|
||||
abstract val label: TextReference
|
||||
|
||||
/** Placeholder (hint) */
|
||||
abstract val placeholder: TextReference
|
||||
|
||||
data class Amount(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
override val label: TextReference,
|
||||
override val placeholder: TextReference,
|
||||
val fiatValue: String,
|
||||
val isError: Boolean,
|
||||
val error: TextReference,
|
||||
) : SendTextField()
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package com.tangem.features.send.impl.presentation.send.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.components.currency.fiaticon.FiatIcon
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.send.state.SendAmountSegmentedButtonsConfig
|
||||
import com.tangem.features.send.impl.presentation.send.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.send.ui.amount.AmountFieldContainer
|
||||
|
||||
@Composable
|
||||
internal fun SendAmountContent(amountState: SendUiState.Content.AmountState) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.common_send),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing16)
|
||||
.align(CenterHorizontally),
|
||||
)
|
||||
AmountFieldContainer(amountState = amountState)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
SegmentedButtons(
|
||||
modifier = Modifier
|
||||
.height(TangemTheme.dimens.size40)
|
||||
.weight(1f),
|
||||
config = amountState.segmentedButtonConfig,
|
||||
onClick = { amountState.clickIntents.onCurrencyChangeClick(it.isFiat) },
|
||||
) {
|
||||
SendAmountCurrencyButton(it)
|
||||
}
|
||||
SecondaryButton(
|
||||
text = stringResource(R.string.send_max_amount),
|
||||
onClick = amountState.clickIntents::onMaxValueClick,
|
||||
size = TangemButtonSize.Text,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius26),
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing8)
|
||||
.height(TangemTheme.dimens.size40),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing10,
|
||||
),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (button.isFiat) {
|
||||
FiatIcon(
|
||||
url = button.iconUrl,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size18),
|
||||
)
|
||||
} else {
|
||||
button.iconState?.let {
|
||||
TokenIcon(
|
||||
state = it,
|
||||
shouldDisplayNetwork = false,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size18),
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = button.title.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.button,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package com.tangem.features.send.impl.presentation.send.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.send.state.SendUiState
|
||||
|
||||
@Composable
|
||||
internal fun SendNavigationButtons(uiState: SendUiState.Content) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
SendSecondaryNavigationButton(uiState)
|
||||
SendPrimaryNavigationButton(
|
||||
uiState = uiState,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendSecondaryNavigationButton(uiState: SendUiState.Content) {
|
||||
AnimatedVisibility(
|
||||
visible = uiState is SendUiState.Content.RecipientState || uiState is SendUiState.Content.FeeState,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing16)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable {
|
||||
// todo add prev click
|
||||
}
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
painter = painterResource(R.drawable.ic_back_24),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendPrimaryNavigationButton(uiState: SendUiState.Content, modifier: Modifier = Modifier) {
|
||||
val buttonTextId = when (uiState) {
|
||||
is SendUiState.Content.AmountState,
|
||||
is SendUiState.Content.RecipientState,
|
||||
is SendUiState.Content.FeeState,
|
||||
-> R.string.common_next
|
||||
is SendUiState.Content.SendState -> R.string.common_send
|
||||
else -> R.string.common_close
|
||||
}
|
||||
AnimatedContent(
|
||||
targetState = buttonTextId,
|
||||
label = "Update send screen state",
|
||||
modifier = modifier,
|
||||
) { textId ->
|
||||
if (uiState is SendUiState.Content.SendState) {
|
||||
PrimaryButtonIconEnd(
|
||||
text = stringResource(textId),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
enabled = uiState.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
// todo add next click
|
||||
},
|
||||
)
|
||||
} else {
|
||||
PrimaryButton(
|
||||
text = stringResource(textId),
|
||||
enabled = uiState.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
// todo add next click
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.features.send.impl.presentation.send.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetDraggableHeader
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.send.state.SendUiState
|
||||
|
||||
@Composable
|
||||
internal fun SendScreen(uiState: SendUiState.Content) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.background(
|
||||
color = TangemTheme.colors.background.tertiary,
|
||||
shape = RoundedCornerShape(
|
||||
topStart = TangemTheme.dimens.radius24,
|
||||
topEnd = TangemTheme.dimens.radius24,
|
||||
),
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
TangemBottomSheetDraggableHeader(
|
||||
color = TangemTheme.colors.background.tertiary,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.scrollable(state = rememberScrollState(), orientation = Orientation.Vertical),
|
||||
) {
|
||||
SendScreenContent(uiState)
|
||||
}
|
||||
SendNavigationButtons(uiState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendScreenContent(uiState: SendUiState.Content) {
|
||||
when (uiState) {
|
||||
is SendUiState.Content.AmountState -> SendAmountContent(uiState)
|
||||
else -> { /* [REDACTED_TODO_COMMENT]*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
package com.tangem.features.send.impl.presentation.send.ui.amount
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.BottomCenter
|
||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.input.OffsetMapping
|
||||
import androidx.compose.ui.text.input.TransformedText
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.send.state.fields.SendTextField
|
||||
|
||||
@Composable
|
||||
internal fun ColumnScope.AmountField(
|
||||
sendField: SendTextField.Amount,
|
||||
cryptoSymbol: String,
|
||||
fiatSymbol: String,
|
||||
isFiat: Boolean,
|
||||
) {
|
||||
val value = if (isFiat) sendField.fiatValue else sendField.value
|
||||
val secondaryValue = if (!isFiat) sendField.fiatValue else sendField.value
|
||||
val symbol = if (isFiat) fiatSymbol else cryptoSymbol
|
||||
val secondarySymbol = if (!isFiat) fiatSymbol else cryptoSymbol
|
||||
|
||||
AmountFieldInner(
|
||||
value = value,
|
||||
placeholder = sendField.placeholder,
|
||||
symbol = symbol,
|
||||
onValueChange = sendField.onValueChange,
|
||||
keyboardOptions = sendField.keyboardOptions,
|
||||
modifier = Modifier
|
||||
.align(CenterHorizontally)
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing24,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(CenterHorizontally)
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing8,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = "$secondaryValue $secondarySymbol",
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.align(BottomCenter)
|
||||
.padding(bottom = TangemTheme.dimens.spacing32),
|
||||
)
|
||||
AmountFieldError(
|
||||
isError = sendField.isError,
|
||||
error = sendField.error,
|
||||
modifier = Modifier
|
||||
.align(BottomCenter)
|
||||
.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountFieldInner(
|
||||
value: String,
|
||||
placeholder: TextReference,
|
||||
symbol: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
keyboardOptions: KeyboardOptions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = modifier
|
||||
.focusRequester(focusRequester)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
textStyle = TangemTheme.typography.h2.copy(
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
keyboardOptions = keyboardOptions,
|
||||
singleLine = true,
|
||||
visualTransformation = AmountVisualTransformation(symbol),
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (value.isBlank()) {
|
||||
Text(
|
||||
text = "${placeholder.resolveReference()} $symbol",
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter),
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountFieldError(isError: Boolean, error: TextReference, modifier: Modifier = Modifier) {
|
||||
AnimatedVisibility(
|
||||
visible = isError,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = error.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.warning,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class AmountVisualTransformation(
|
||||
private val symbol: String,
|
||||
) : VisualTransformation {
|
||||
override fun filter(text: AnnotatedString): TransformedText {
|
||||
return TransformedText(
|
||||
buildAnnotatedString {
|
||||
append(text)
|
||||
if (text.isNotBlank()) {
|
||||
append(" ")
|
||||
append(symbol)
|
||||
}
|
||||
},
|
||||
object : OffsetMapping {
|
||||
override fun originalToTransformed(offset: Int): Int {
|
||||
return text.length
|
||||
}
|
||||
|
||||
override fun transformedToOriginal(offset: Int): Int {
|
||||
return text.length
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.features.send.impl.presentation.send.ui.amount
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.send.state.SendUiState
|
||||
|
||||
@Composable
|
||||
internal fun AmountFieldContainer(amountState: SendUiState.Content.AmountState, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing4,
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
Text(
|
||||
text = amountState.walletName,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing14)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
Text(
|
||||
text = amountState.walletBalance,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing2)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
TokenIcon(
|
||||
state = amountState.tokenIconState,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing32)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
AmountField(
|
||||
sendField = amountState.amountTextField,
|
||||
cryptoSymbol = amountState.cryptoCurrencyStatus.currency.symbol,
|
||||
fiatSymbol = amountState.appCurrency.symbol,
|
||||
isFiat = amountState.isFiatValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.send.impl.presentation.send.viewmodel
|
||||
|
||||
interface SendClickIntents {
|
||||
|
||||
fun onNextClick()
|
||||
|
||||
fun onPrevClick()
|
||||
|
||||
fun onAmountValueChange(value: String)
|
||||
|
||||
fun onCurrencyChangeClick(isFiat: Boolean)
|
||||
|
||||
fun onMaxValueClick()
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package com.tangem.features.send.impl.presentation.send.viewmodel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.*
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.send.impl.presentation.send.state.SendStateFactory
|
||||
import com.tangem.features.send.impl.presentation.send.state.SendUiState
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class SendViewModel @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), DefaultLifecycleObserver, SendClickIntents {
|
||||
|
||||
private val userWalletId: UserWalletId = savedStateHandle.get<String>(SendRouter.USER_WALLET_ID_KEY)
|
||||
?.let { stringValue -> UserWalletId(stringValue) }
|
||||
?: error("This screen can't open without `UserWalletId`")
|
||||
|
||||
private val cryptoCurrency: CryptoCurrency = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY]
|
||||
?: error("This screen can't open without `CryptoCurrency`")
|
||||
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
||||
|
||||
private val stateFactory = SendStateFactory(
|
||||
clickIntents = this,
|
||||
currentStateProvider = Provider { uiState },
|
||||
userWalletProvider = Provider { userWallet },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
)
|
||||
|
||||
var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState())
|
||||
private set
|
||||
|
||||
private var userWallet: UserWallet? = null
|
||||
|
||||
private var balanceJobHolder = JobHolder()
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
subscribeOnCurrencyStatusUpdates(owner)
|
||||
}
|
||||
|
||||
private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
getUserWalletUseCase(userWalletId).fold(
|
||||
ifRight = { wallet ->
|
||||
userWallet = wallet
|
||||
getCurrencyStatusUpdates(owner, wallet)
|
||||
},
|
||||
ifLeft = {
|
||||
// TODO add error handling
|
||||
return@launch
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCurrencyStatusUpdates(owner: LifecycleOwner, wallet: UserWallet) {
|
||||
val isSingleWallet = wallet.scanResponse.walletData?.token != null && !wallet.isMultiCurrency
|
||||
getCurrencyStatusUpdatesUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
derivationPath = cryptoCurrency.network.derivationPath,
|
||||
isSingleWalletWithTokens = isSingleWallet,
|
||||
)
|
||||
.flowWithLifecycle(owner.lifecycle)
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { either ->
|
||||
uiState = stateFactory.getAmountState(
|
||||
cryptoCurrencyStatus = either,
|
||||
)
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(balanceJobHolder)
|
||||
}
|
||||
|
||||
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
|
||||
return getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = AppCurrency.Default,
|
||||
)
|
||||
}
|
||||
|
||||
// region screen state navigation
|
||||
override fun onNextClick() {
|
||||
when (uiState) {
|
||||
is SendUiState.Content.AmountState -> onRecipientStateClick()
|
||||
is SendUiState.Content.RecipientState -> onFeeStateClick()
|
||||
else -> {
|
||||
// todo implement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrevClick() {
|
||||
// todo implement
|
||||
}
|
||||
|
||||
private fun onRecipientStateClick() {
|
||||
stateFactory.getOnReceiveState()
|
||||
}
|
||||
|
||||
private fun onFeeStateClick() {
|
||||
// todo implement
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region amount state clicks
|
||||
override fun onCurrencyChangeClick(isFiat: Boolean) {
|
||||
uiState = stateFactory.getOnCurrencyChangedState(isFiat)
|
||||
}
|
||||
|
||||
override fun onAmountValueChange(value: String) {
|
||||
uiState = stateFactory.getOnAmountValueChange(value)
|
||||
}
|
||||
|
||||
override fun onMaxValueClick() {
|
||||
val amountState = uiState as? SendUiState.Content.AmountState ?: return
|
||||
|
||||
val amount = if (amountState.isFiatValue) {
|
||||
amountState.cryptoCurrencyStatus.value.fiatAmount
|
||||
} else {
|
||||
amountState.cryptoCurrencyStatus.value.amount
|
||||
}
|
||||
onAmountValueChange(amount?.toPlainString() ?: "0.00")
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ dependencies {
|
|||
|
||||
/** Domain modules **/
|
||||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.activity.compose)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import androidx.lifecycle.*
|
|||
import com.tangem.common.Provider
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.utils.InputNumberFormatter
|
||||
import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.feature.swap.analytics.SwapEvents
|
||||
|
|
@ -49,7 +49,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
private val blockchainInteractor: BlockchainInteractor,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val listenToFlipsUseCase: ListenToFlipsUseCase,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), DefaultLifecycleObserver {
|
||||
|
|
@ -96,10 +96,10 @@ internal class SwapViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
isBalanceHiddenUseCase()
|
||||
getBalanceHidingSettingsUseCase()
|
||||
.flowWithLifecycle(owner.lifecycle)
|
||||
.onEach { hidden ->
|
||||
isBalanceHidden = hidden
|
||||
.onEach {
|
||||
isBalanceHidden = it.isBalanceHidden
|
||||
withContext(dispatchers.main) {
|
||||
uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ dependencies {
|
|||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
|
||||
/** Feature Apis */
|
||||
implementation(projects.features.tokendetails.api)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
|
|||
iconResId = iconResId,
|
||||
buttonsState = buttonsState,
|
||||
),
|
||||
)
|
||||
) {
|
||||
|
||||
sealed class Informational(
|
||||
title: TextReference,
|
||||
|
|
@ -108,5 +108,35 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
|
|||
id = R.string.warning_send_blocked_pending_transactions_message,
|
||||
formatArgs = wrappedList(coinSymbol),
|
||||
),
|
||||
)
|
||||
) {
|
||||
data class RentInfo(
|
||||
private val rentInfo: CryptoCurrencyWarning.Rent,
|
||||
private val onCloseClick: () -> Unit,
|
||||
) : Informational(
|
||||
title = TextReference.Res(R.string.warning_rent_fee_title),
|
||||
subtitle = TextReference.Res(
|
||||
id = R.string.warning_solana_rent_fee_message,
|
||||
formatArgs = wrappedList(rentInfo.rent, rentInfo.exemptionAmount),
|
||||
),
|
||||
onCloseClick = onCloseClick,
|
||||
)
|
||||
|
||||
data class ExistentialDeposit(
|
||||
private val existentialInfo: CryptoCurrencyWarning.ExistentialDeposit,
|
||||
) : Informational(
|
||||
title = resourceReference(R.string.warning_existential_deposit_title),
|
||||
subtitle = TextReference.Res(
|
||||
id = R.string.warning_existential_deposit_message,
|
||||
formatArgs = wrappedList(existentialInfo.currencyName, existentialInfo.edStringValueWithSymbol),
|
||||
),
|
||||
)
|
||||
|
||||
class NetworksNoAccount(val network: String, val symbol: String, val amount: String) : Informational(
|
||||
title = resourceReference(R.string.warning_no_account_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.no_account_generic,
|
||||
wrappedList(network, amount, symbol),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@ internal class TokenDetailsLoadedBalanceConverter(
|
|||
return state.copy(
|
||||
tokenBalanceBlockState = TokenDetailsBalanceBlockState.Error(state.tokenBalanceBlockState.actionButtons),
|
||||
marketPriceBlockState = MarketPriceBlockState.Error(state.marketPriceBlockState.currencySymbol),
|
||||
notifications = persistentListOf(TokenDetailsNotification.NetworksUnreachable),
|
||||
notifications = persistentListOf(TokenDetailsNotification.Warning.NetworksUnreachable),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,20 +19,20 @@ internal class TokenDetailsNotificationConverter(
|
|||
|
||||
fun removeRentInfo(currentState: TokenDetailsState): ImmutableList<TokenDetailsNotification> {
|
||||
val newNotifications = currentState.notifications.toMutableList()
|
||||
newNotifications.removeBy { it is TokenDetailsNotification.RentInfo }
|
||||
newNotifications.removeBy { it is TokenDetailsNotification.Informational.RentInfo }
|
||||
return newNotifications.toImmutableList()
|
||||
}
|
||||
|
||||
private fun mapToNotification(warning: CryptoCurrencyWarning): TokenDetailsNotification {
|
||||
return when (warning) {
|
||||
is CryptoCurrencyWarning.BalanceNotEnoughForFee -> TokenDetailsNotification.NetworkFee(
|
||||
is CryptoCurrencyWarning.BalanceNotEnoughForFee -> TokenDetailsNotification.Warning.NetworkFee(
|
||||
feeInfo = warning,
|
||||
onBuyClick = { clickIntents.onBuyCoinClick(warning.coinCurrency) },
|
||||
)
|
||||
is CryptoCurrencyWarning.ExistentialDeposit -> TokenDetailsNotification.ExistentialDeposit(
|
||||
is CryptoCurrencyWarning.ExistentialDeposit -> TokenDetailsNotification.Informational.ExistentialDeposit(
|
||||
existentialInfo = warning,
|
||||
)
|
||||
is CryptoCurrencyWarning.Rent -> TokenDetailsNotification.RentInfo(
|
||||
is CryptoCurrencyWarning.Rent -> TokenDetailsNotification.Informational.RentInfo(
|
||||
rentInfo = warning,
|
||||
onCloseClick = clickIntents::onCloseRentInfoNotification,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
|
|
@ -65,7 +65,7 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
|
||||
private val removeCurrencyUseCase: RemoveCurrencyUseCase,
|
||||
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
|
||||
private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val listenToFlipsUseCase: ListenToFlipsUseCase,
|
||||
private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
|
|
@ -117,11 +117,11 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleBalanceHiding(owner: LifecycleOwner) {
|
||||
isBalanceHiddenUseCase()
|
||||
getBalanceHidingSettingsUseCase()
|
||||
.flowWithLifecycle(owner.lifecycle)
|
||||
.onEach { hidden ->
|
||||
.onEach {
|
||||
uiState = stateFactory.getStateWithUpdatedHidden(
|
||||
isBalanceHidden = hidden,
|
||||
isBalanceHidden = it.isBalanceHidden,
|
||||
)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ dependencies {
|
|||
implementation(projects.domain.appCurrency)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
|
||||
//TODO: Create api/impl modules for onboarding [REDACTED_JIRA]
|
||||
implementation(projects.features.onboarding)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
|||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
|
|
@ -88,7 +89,7 @@ internal object WalletPreviewData {
|
|||
}
|
||||
|
||||
val coinIconState
|
||||
get() = TokenItemState.IconState.CoinIcon(
|
||||
get() = TokenIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = R.drawable.img_polygon_22,
|
||||
isGrayscale = false,
|
||||
|
|
@ -96,7 +97,7 @@ internal object WalletPreviewData {
|
|||
)
|
||||
|
||||
private val tokenIconState
|
||||
get() = TokenItemState.IconState.TokenIcon(
|
||||
get() = TokenIconState.TokenIcon(
|
||||
url = null,
|
||||
networkBadgeIconResId = R.drawable.img_polygon_22,
|
||||
fallbackTint = TangemColorPalette.Black,
|
||||
|
|
@ -106,7 +107,7 @@ internal object WalletPreviewData {
|
|||
)
|
||||
|
||||
private val customTokenIconState
|
||||
get() = TokenItemState.IconState.CustomTokenIcon(
|
||||
get() = TokenIconState.CustomTokenIcon(
|
||||
tint = TangemColorPalette.Black,
|
||||
background = TangemColorPalette.Meadow,
|
||||
networkBadgeIconResId = R.drawable.img_polygon_22,
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
|
||||
import com.tangem.core.ui.extensions.rememberHapticFeedback
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
|
||||
import com.tangem.feature.wallet.presentation.common.component.token.*
|
||||
import com.tangem.feature.wallet.presentation.common.component.token.icon.TokenIcon
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import org.burnoutcrew.reorderable.ReorderableLazyListState
|
||||
import kotlin.math.max
|
||||
|
|
|
|||
|
|
@ -1,175 +0,0 @@
|
|||
package com.tangem.feature.wallet.presentation.common.component.token.icon
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
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.ColorFilter
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
internal fun ContentIcon(
|
||||
icon: TokenItemState.IconState,
|
||||
alpha: Float,
|
||||
colorFilter: ColorFilter?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (icon) {
|
||||
is TokenItemState.IconState.CoinIcon -> CoinIcon(
|
||||
modifier = modifier,
|
||||
url = icon.url,
|
||||
fallbackResId = icon.fallbackResId,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
)
|
||||
is TokenItemState.IconState.TokenIcon -> TokenIcon(
|
||||
modifier = modifier,
|
||||
url = icon.url,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
errorIcon = {
|
||||
CustomTokenIcon(
|
||||
modifier = modifier,
|
||||
tint = icon.fallbackTint,
|
||||
background = icon.fallbackBackground,
|
||||
alpha = alpha,
|
||||
)
|
||||
},
|
||||
)
|
||||
is TokenItemState.IconState.CustomTokenIcon -> CustomTokenIcon(
|
||||
modifier = modifier,
|
||||
tint = icon.tint,
|
||||
background = icon.background,
|
||||
alpha = alpha,
|
||||
)
|
||||
TokenItemState.IconState.Loading,
|
||||
TokenItemState.IconState.Locked,
|
||||
-> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CoinIcon(
|
||||
url: String?,
|
||||
@DrawableRes fallbackResId: Int,
|
||||
alpha: Float,
|
||||
colorFilter: ColorFilter?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url
|
||||
|
||||
DefaultCurrencyIcon(
|
||||
modifier = modifier,
|
||||
iconData = iconData,
|
||||
errorIcon = {
|
||||
Image(
|
||||
painter = painterResource(id = fallbackResId),
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenIcon(
|
||||
url: String?,
|
||||
alpha: Float,
|
||||
colorFilter: ColorFilter?,
|
||||
errorIcon: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (url == null) {
|
||||
errorIcon()
|
||||
} else {
|
||||
DefaultCurrencyIcon(
|
||||
modifier = modifier,
|
||||
iconData = url,
|
||||
errorIcon = errorIcon,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CustomTokenIcon(tint: Color, background: Color, alpha: Float, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = background.copy(alpha = alpha),
|
||||
shape = CircleShape,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.matchParentSize(),
|
||||
painter = painterResource(id = R.drawable.ic_custom_token_44),
|
||||
tint = tint.copy(alpha = alpha),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private inline fun DefaultCurrencyIcon(
|
||||
iconData: Any,
|
||||
alpha: Float,
|
||||
colorFilter: ColorFilter?,
|
||||
crossinline errorIcon: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
|
||||
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
SubcomposeAsyncImage(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = iconBackgroundColor,
|
||||
shape = TangemTheme.shapes.roundedCorners8,
|
||||
),
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(iconData)
|
||||
.crossfade(enable = true)
|
||||
.allowHardware(false)
|
||||
.listener(
|
||||
onSuccess = { _, result ->
|
||||
if (isDarkTheme) {
|
||||
coroutineScope.launch {
|
||||
val color = ImageBackgroundContrastChecker(
|
||||
drawable = result.drawable,
|
||||
backgroundColor = itemBackgroundColor,
|
||||
).getContrastColorIfNeeded(isDarkTheme)
|
||||
iconBackgroundColor = color
|
||||
}
|
||||
}
|
||||
},
|
||||
).build(),
|
||||
loading = { LoadingIcon() },
|
||||
error = { errorIcon() },
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package com.tangem.feature.wallet.presentation.common.component.token.icon
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
internal fun NetworkBadge(
|
||||
@DrawableRes iconResId: Int,
|
||||
alpha: Float,
|
||||
colorFilter: ColorFilter?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(TangemTheme.dimens.size18)
|
||||
.background(
|
||||
color = TangemTheme.colors.background.primary,
|
||||
shape = CircleShape,
|
||||
),
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.padding(all = TangemTheme.dimens.spacing2)
|
||||
.matchParentSize(),
|
||||
painter = painterResource(id = iconResId),
|
||||
colorFilter = colorFilter,
|
||||
alpha = alpha,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun CustomBadge(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(TangemTheme.dimens.size12)
|
||||
.background(
|
||||
color = TangemTheme.colors.background.primary,
|
||||
shape = CircleShape,
|
||||
),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(all = TangemTheme.dimens.spacing2)
|
||||
.matchParentSize()
|
||||
.background(
|
||||
color = TangemTheme.colors.icon.informative,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
package com.tangem.feature.wallet.presentation.common.component.token.icon
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.ColorFilter
|
||||
import androidx.compose.ui.graphics.ColorMatrix
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.IconState as TokenIconState
|
||||
|
||||
private const val GRAY_SCALE_SATURATION = 0f
|
||||
private const val GRAY_SCALE_ALPHA = 0.4f
|
||||
private const val NORMAL_ALPHA = 1f
|
||||
|
||||
@Composable
|
||||
internal fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier) {
|
||||
BaseContainer(modifier = modifier) {
|
||||
val iconModifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(TangemTheme.dimens.size36)
|
||||
|
||||
when (state) {
|
||||
is TokenIconState.Loading -> LoadingIcon(modifier = iconModifier)
|
||||
is TokenIconState.Locked -> LockedIcon(modifier = iconModifier)
|
||||
is TokenIconState.CoinIcon,
|
||||
is TokenIconState.CustomTokenIcon,
|
||||
is TokenIconState.TokenIcon,
|
||||
-> {
|
||||
ContentIconContainer(modifier = iconModifier, icon = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun LoadingIcon(modifier: Modifier = Modifier) {
|
||||
CircleShimmer(modifier = modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LockedIcon(modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(
|
||||
color = TangemTheme.colors.field.primary,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.ContentIconContainer(icon: TokenIconState, modifier: Modifier = Modifier) {
|
||||
val networkBadgeOffset = TangemTheme.dimens.spacing4
|
||||
val (alpha, colorFilter) = remember(icon.isGrayscale) {
|
||||
if (icon.isGrayscale) {
|
||||
GRAY_SCALE_ALPHA to GrayscaleColorFilter
|
||||
} else {
|
||||
NORMAL_ALPHA to null
|
||||
}
|
||||
}
|
||||
|
||||
ContentIcon(
|
||||
modifier = modifier,
|
||||
icon = icon,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
)
|
||||
|
||||
if (icon.networkBadgeIconResId != null) {
|
||||
NetworkBadge(
|
||||
modifier = Modifier
|
||||
.offset(x = networkBadgeOffset, y = -networkBadgeOffset)
|
||||
.align(Alignment.TopEnd),
|
||||
iconResId = requireNotNull(icon.networkBadgeIconResId),
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
)
|
||||
}
|
||||
|
||||
if (icon.showCustomBadge) {
|
||||
CustomBadge(modifier = Modifier.align(Alignment.BottomEnd))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private inline fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) {
|
||||
Box(modifier = modifier.size(size = TangemTheme.dimens.size40), content = content)
|
||||
}
|
||||
|
||||
private val GrayscaleColorFilter: ColorFilter
|
||||
get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) })
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
package com.tangem.feature.wallet.presentation.common.state
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
||||
|
||||
/** Token item state */
|
||||
@Immutable
|
||||
|
|
@ -11,7 +10,7 @@ internal sealed class TokenItemState {
|
|||
|
||||
abstract val id: String
|
||||
|
||||
abstract val iconState: IconState
|
||||
abstract val iconState: TokenIconState
|
||||
|
||||
abstract val titleState: TitleState
|
||||
|
||||
|
|
@ -24,7 +23,7 @@ internal sealed class TokenItemState {
|
|||
/** Loading token state */
|
||||
data class Loading(
|
||||
override val id: String,
|
||||
override val iconState: IconState,
|
||||
override val iconState: TokenIconState,
|
||||
override val titleState: TitleState.Content,
|
||||
) : TokenItemState() {
|
||||
override val fiatAmountState: FiatAmountState = FiatAmountState.Loading
|
||||
|
|
@ -34,7 +33,7 @@ internal sealed class TokenItemState {
|
|||
|
||||
/** Locked token state */
|
||||
data class Locked(override val id: String) : TokenItemState() {
|
||||
override val iconState: IconState = IconState.Locked
|
||||
override val iconState: TokenIconState = TokenIconState.Locked
|
||||
override val titleState: TitleState = TitleState.Locked
|
||||
override val fiatAmountState: FiatAmountState = FiatAmountState.Locked
|
||||
override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Locked
|
||||
|
|
@ -52,7 +51,7 @@ internal sealed class TokenItemState {
|
|||
*/
|
||||
data class Content(
|
||||
override val id: String,
|
||||
override val iconState: IconState,
|
||||
override val iconState: TokenIconState,
|
||||
override val titleState: TitleState,
|
||||
override val fiatAmountState: FiatAmountState,
|
||||
override val cryptoAmountState: CryptoAmountState.Content,
|
||||
|
|
@ -70,7 +69,7 @@ internal sealed class TokenItemState {
|
|||
*/
|
||||
data class Draggable(
|
||||
override val id: String,
|
||||
override val iconState: IconState,
|
||||
override val iconState: TokenIconState,
|
||||
override val titleState: TitleState,
|
||||
override val cryptoAmountState: CryptoAmountState,
|
||||
) : TokenItemState() {
|
||||
|
|
@ -89,7 +88,7 @@ internal sealed class TokenItemState {
|
|||
*/
|
||||
data class Unreachable(
|
||||
override val id: String,
|
||||
override val iconState: IconState,
|
||||
override val iconState: TokenIconState,
|
||||
override val titleState: TitleState,
|
||||
val onItemClick: () -> Unit,
|
||||
val onItemLongClick: () -> Unit,
|
||||
|
|
@ -109,7 +108,7 @@ internal sealed class TokenItemState {
|
|||
*/
|
||||
data class NoAddress(
|
||||
override val id: String,
|
||||
override val iconState: IconState,
|
||||
override val iconState: TokenIconState,
|
||||
override val titleState: TitleState,
|
||||
val onItemLongClick: () -> Unit,
|
||||
) : TokenItemState() {
|
||||
|
|
@ -118,84 +117,6 @@ internal sealed class TokenItemState {
|
|||
override val priceChangeState: PriceChangeState? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the various states an icon can be in.
|
||||
*/
|
||||
@Immutable
|
||||
sealed class IconState {
|
||||
|
||||
abstract val isGrayscale: Boolean
|
||||
abstract val showCustomBadge: Boolean
|
||||
abstract val networkBadgeIconResId: Int?
|
||||
|
||||
/**
|
||||
* Represents a coin icon.
|
||||
*
|
||||
* @property url The URL where the coin icon can be fetched from. May be `null` if not found.
|
||||
* @property fallbackResId The drawable resource ID to be used as a fallback if the URL is not available.
|
||||
* @property isGrayscale Specifies whether to show the icon in grayscale.
|
||||
* @property showCustomBadge Specifies whether to show the custom token badge.
|
||||
*/
|
||||
data class CoinIcon(
|
||||
val url: String?,
|
||||
@DrawableRes val fallbackResId: Int,
|
||||
override val isGrayscale: Boolean,
|
||||
override val showCustomBadge: Boolean,
|
||||
) : IconState() {
|
||||
|
||||
override val networkBadgeIconResId: Int? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a token icon.
|
||||
*
|
||||
* @property url The URL where the token icon can be fetched from. May be `null` if not found.
|
||||
* @property networkBadgeIconResId The drawable resource ID for the network badge.
|
||||
* @property isGrayscale Specifies whether to show the icon in grayscale.
|
||||
* @property showCustomBadge Specifies whether to show the custom token badge.
|
||||
* @property fallbackTint The color to be used for tinting the fallback icon.
|
||||
* @property fallbackBackground The background color to be used for the fallback icon.
|
||||
*/
|
||||
data class TokenIcon(
|
||||
val url: String?,
|
||||
@DrawableRes override val networkBadgeIconResId: Int,
|
||||
override val isGrayscale: Boolean,
|
||||
override val showCustomBadge: Boolean,
|
||||
val fallbackTint: Color,
|
||||
val fallbackBackground: Color,
|
||||
) : IconState()
|
||||
|
||||
/**
|
||||
* Represents a custom token icon.
|
||||
*
|
||||
* @property tint The color to be used for tinting the icon.
|
||||
* @property background The background color to be used for the icon.
|
||||
* @property networkBadgeIconResId The drawable resource ID for the network badge.
|
||||
* @property isGrayscale Specifies whether to show the icon in grayscale.
|
||||
*/
|
||||
data class CustomTokenIcon(
|
||||
val tint: Color,
|
||||
val background: Color,
|
||||
@DrawableRes override val networkBadgeIconResId: Int,
|
||||
override val isGrayscale: Boolean,
|
||||
) : IconState() {
|
||||
|
||||
override val showCustomBadge: Boolean = true
|
||||
}
|
||||
|
||||
object Loading : IconState() {
|
||||
override val isGrayscale: Boolean = false
|
||||
override val showCustomBadge: Boolean = false
|
||||
override val networkBadgeIconResId: Int? = null
|
||||
}
|
||||
|
||||
object Locked : IconState() {
|
||||
override val isGrayscale: Boolean = false
|
||||
override val showCustomBadge: Boolean = false
|
||||
override val networkBadgeIconResId: Int? = null
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class TitleState {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
package com.tangem.feature.wallet.presentation.common.utils
|
||||
|
||||
import com.tangem.core.ui.extensions.getTintForTokenIcon
|
||||
import com.tangem.core.ui.extensions.networkIconResId
|
||||
import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrencyStatus, TokenItemState.IconState> {
|
||||
|
||||
override fun convert(value: CryptoCurrencyStatus): TokenItemState.IconState {
|
||||
return when (val currency = value.currency) {
|
||||
is CryptoCurrency.Coin -> getIconStateForCoin(currency, value.value.isError)
|
||||
is CryptoCurrency.Token -> getIconStateForToken(currency, value.value.isError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getIconStateForCoin(
|
||||
coin: CryptoCurrency.Coin,
|
||||
isUnreachable: Boolean,
|
||||
): TokenItemState.IconState.CoinIcon {
|
||||
return TokenItemState.IconState.CoinIcon(
|
||||
url = coin.iconUrl,
|
||||
fallbackResId = coin.networkIconResId,
|
||||
isGrayscale = coin.network.isTestnet || isUnreachable,
|
||||
showCustomBadge = coin.isCustom,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getIconStateForToken(token: CryptoCurrency.Token, isErrorStatus: Boolean): TokenItemState.IconState {
|
||||
val isGrayscale = token.network.isTestnet || isErrorStatus
|
||||
val background = token.tryGetBackgroundForTokenIcon(isGrayscale)
|
||||
val tint = getTintForTokenIcon(background)
|
||||
|
||||
return if (token.isCustom && token.iconUrl == null) {
|
||||
TokenItemState.IconState.CustomTokenIcon(
|
||||
tint = tint,
|
||||
background = background,
|
||||
networkBadgeIconResId = token.networkIconResId,
|
||||
isGrayscale = isGrayscale,
|
||||
)
|
||||
} else {
|
||||
TokenItemState.IconState.TokenIcon(
|
||||
url = token.iconUrl,
|
||||
networkBadgeIconResId = token.networkIconResId,
|
||||
isGrayscale = isGrayscale,
|
||||
fallbackTint = tint,
|
||||
fallbackBackground = background,
|
||||
showCustomBadge = token.isCustom, // `true` for tokens with custom derivation
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
|
||||
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
|
||||
import com.tangem.domain.tokens.GetTokenListUseCase
|
||||
|
|
@ -38,7 +38,7 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
private val toggleTokenListSortingUseCase: ToggleTokenListSortingUseCase,
|
||||
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val listenToFlipsUseCase: ListenToFlipsUseCase,
|
||||
private val analyticsEventsHandler: AnalyticsEventHandler,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -74,10 +74,10 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
override fun onCreate(owner: LifecycleOwner) {
|
||||
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened)
|
||||
|
||||
isBalanceHiddenUseCase()
|
||||
getBalanceHidingSettingsUseCase()
|
||||
.flowWithLifecycle(owner.lifecycle)
|
||||
.onEach { hidden ->
|
||||
isBalanceHidden = hidden
|
||||
.onEach {
|
||||
isBalanceHidden = it.isBalanceHidden
|
||||
stateHolder.updateHiddenState(isBalanceHidden)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ internal class DefaultWalletRouter(private val reduxNavController: ReduxNavContr
|
|||
override fun isWalletLastScreen(): Boolean = reduxNavController.getBackStack().lastOrNull() == AppScreen.Wallet
|
||||
|
||||
override fun openManageTokensScreen() {
|
||||
reduxNavController.navigate(action = NavigationAction.NavigateTo(AppScreen.AddTokens))
|
||||
reduxNavController.navigate(action = NavigationAction.NavigateTo(AppScreen.ManageTokens))
|
||||
}
|
||||
|
||||
override fun openScanFailedDialog() {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
|
|
|||
|
|
@ -159,6 +159,18 @@ internal class WalletNotificationsListFactory(
|
|||
addMissingAddressesNotification(maybeMissedAddressCurrencies)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addInformationalNotifications(
|
||||
cardTypesResolver: CardTypesResolver,
|
||||
maybeMissedAddressCurrencies: Either<GetCurrenciesError, List<CryptoCurrency>>,
|
||||
) {
|
||||
addIf(
|
||||
element = WalletNotification.Informational.DemoCard,
|
||||
condition = isDemoCardUseCase(cardId = cardTypesResolver.getCardId()),
|
||||
)
|
||||
|
||||
addMissingAddressesNotification(maybeMissedAddressCurrencies)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addIf(element: WalletNotification, condition: Boolean) {
|
||||
if (condition) {
|
||||
add(element = element)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import com.tangem.crypto.hdWallet.DerivationPath
|
|||
import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
|
||||
import com.tangem.domain.card.DerivePublicKeysUseCase
|
||||
import com.tangem.domain.card.SetCardWasScannedUseCase
|
||||
|
|
@ -116,6 +116,7 @@ internal class WalletViewModel @Inject constructor(
|
|||
private val canUseBiometryUseCase: CanUseBiometryUseCase,
|
||||
private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
|
||||
private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val listenToFlipsUseCase: ListenToFlipsUseCase,
|
||||
private val removeCurrencyUseCase: RemoveCurrencyUseCase,
|
||||
private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase,
|
||||
|
|
@ -212,12 +213,12 @@ internal class WalletViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
isBalanceHiddenUseCase()
|
||||
getBalanceHidingSettingsUseCase()
|
||||
.flowWithLifecycle(owner.lifecycle)
|
||||
.onEach { hidden ->
|
||||
isBalanceHidden = hidden
|
||||
WalletStateCache.updateAll { copySealed(isBalanceHidden = hidden) }
|
||||
uiState = stateFactory.getHiddenBalanceState(isBalanceHidden = hidden)
|
||||
.onEach {
|
||||
isBalanceHidden = it.isBalanceHidden
|
||||
WalletStateCache.updateAll { copySealed(isBalanceHidden = it.isBalanceHidden) }
|
||||
uiState = stateFactory.getHiddenBalanceState(isBalanceHidden = it.isBalanceHidden)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue