Updated on 2026-08-14
This commit is contained in:
commit
f0e4aec9a4
858 changed files with 13724 additions and 5705 deletions
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>BooleanPropertyNaming:AppRoute.kt$AppRoute.CreateWalletBackup$val setAccessCode: Boolean = false</ID>
|
||||
<ID>NestedScopeFunctions:PayloadToDeeplinkConverter.kt$PayloadToDeeplinkConverter$let { addQueryParam(NAME_KEY, it) }</ID>
|
||||
<ID>NestedScopeFunctions:PayloadToDeeplinkConverter.kt$PayloadToDeeplinkConverter$let { addQueryParam(TRANSACTION_ID_KEY, it) }</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -346,7 +346,9 @@ sealed class AppRoute(val path: String) : Route {
|
|||
object CreateHardwareWallet : AppRoute(path = "/create_hardware_wallet")
|
||||
|
||||
@Serializable
|
||||
object CreateMobileWallet : AppRoute(path = "/create_mobile_wallet")
|
||||
data class CreateMobileWallet(
|
||||
val source: String,
|
||||
) : AppRoute(path = "/create_mobile_wallet")
|
||||
|
||||
@Serializable
|
||||
data class UpgradeWallet(
|
||||
|
|
@ -368,7 +370,7 @@ sealed class AppRoute(val path: String) : Route {
|
|||
val analyticsSource: String,
|
||||
val analyticsAction: String,
|
||||
val isUpgradeFlow: Boolean = false,
|
||||
val setAccessCode: Boolean = false,
|
||||
val shouldSetAccessCode: Boolean = false,
|
||||
) : AppRoute(path = "/create_wallet_backup/${userWalletId.stringValue}")
|
||||
|
||||
@Serializable
|
||||
|
|
@ -431,13 +433,20 @@ sealed class AppRoute(val path: String) : Route {
|
|||
@Serializable
|
||||
data class Deeplink(
|
||||
val deeplink: String,
|
||||
val userWalletId: UserWalletId?,
|
||||
) : Mode()
|
||||
|
||||
@Serializable
|
||||
data class ContinueOnboarding(
|
||||
val userWalletId: UserWalletId?,
|
||||
val userWalletId: UserWalletId,
|
||||
) : Mode()
|
||||
|
||||
@Serializable
|
||||
data class FromBannerOnMain(
|
||||
val userWalletId: UserWalletId,
|
||||
) : Mode()
|
||||
|
||||
@Serializable
|
||||
data object FromBannerInSettings : Mode()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,8 +55,12 @@ object PayloadToDeeplinkConverter : Converter<Map<String, String>, String?> {
|
|||
addQueryParam(DERIVATION_PATH_KEY, derivationPath)
|
||||
}
|
||||
|
||||
transactionId?.let { addQueryParam(TRANSACTION_ID_KEY, it) }
|
||||
name?.let { addQueryParam(NAME_KEY, it) }
|
||||
if (transactionId != null) {
|
||||
addQueryParam(TRANSACTION_ID_KEY, transactionId)
|
||||
}
|
||||
if (name != null) {
|
||||
addQueryParam(NAME_KEY, name)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
package com.tangem.common.test.data.staking
|
||||
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolExitQueueDTO
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolStakeDTO
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Factory for creating mock P2P ETH Pool account responses for testing
|
||||
*/
|
||||
object MockP2PEthPoolAccountResponseFactory {
|
||||
|
||||
private val defaultStakingId = StakingID(
|
||||
integrationId = "p2p-ethereum-pooled",
|
||||
address = "0x5aa711F440Eb6d4361148bBD89d03464628ace84",
|
||||
)
|
||||
|
||||
const val defaultVaultAddress = "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
|
||||
|
||||
fun createWithBalance(
|
||||
stakingId: StakingID = defaultStakingId,
|
||||
vaultAddress: String = defaultVaultAddress,
|
||||
stakedAmount: BigDecimal = BigDecimal("1.5"),
|
||||
earnedAmount: BigDecimal = BigDecimal("0.05"),
|
||||
): P2PEthPoolAccountResponse {
|
||||
return P2PEthPoolAccountResponse(
|
||||
delegatorAddress = stakingId.address,
|
||||
vaultAddress = vaultAddress,
|
||||
stake = P2PEthPoolStakeDTO(
|
||||
assets = stakedAmount,
|
||||
totalEarnedAssets = earnedAmount,
|
||||
),
|
||||
availableToUnstake = stakedAmount,
|
||||
availableToWithdraw = BigDecimal.ZERO,
|
||||
exitQueue = P2PEthPoolExitQueueDTO(
|
||||
total = 0.0,
|
||||
requests = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createWithEmptyBalance(
|
||||
stakingId: StakingID = defaultStakingId,
|
||||
vaultAddress: String = defaultVaultAddress,
|
||||
): P2PEthPoolAccountResponse {
|
||||
return P2PEthPoolAccountResponse(
|
||||
delegatorAddress = stakingId.address,
|
||||
vaultAddress = vaultAddress,
|
||||
stake = P2PEthPoolStakeDTO(
|
||||
assets = BigDecimal.ZERO,
|
||||
totalEarnedAssets = BigDecimal.ZERO,
|
||||
),
|
||||
availableToUnstake = BigDecimal.ZERO,
|
||||
availableToWithdraw = BigDecimal.ZERO,
|
||||
exitQueue = P2PEthPoolExitQueueDTO(
|
||||
total = 0.0,
|
||||
requests = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createMockVault(vaultAddress: String = defaultVaultAddress): P2PEthPoolVault {
|
||||
return P2PEthPoolVault(
|
||||
vaultAddress = vaultAddress,
|
||||
displayName = "Test Vault",
|
||||
apy = BigDecimal("3.5"),
|
||||
baseApy = BigDecimal("3.0"),
|
||||
capacity = BigDecimal("10000"),
|
||||
totalAssets = BigDecimal("5000"),
|
||||
feePercent = BigDecimal("10"),
|
||||
isPrivate = false,
|
||||
isGenesis = false,
|
||||
isSmoothingPool = false,
|
||||
isErc20 = false,
|
||||
tokenName = "Test Token",
|
||||
tokenSymbol = "TT",
|
||||
createdAt = 0L,
|
||||
)
|
||||
}
|
||||
}
|
||||
1
common/ui-markets/.gitignore
vendored
Normal file
1
common/ui-markets/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
29
common/ui-markets/build.gradle.kts
Normal file
29
common/ui-markets/build.gradle.kts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.common.ui.markets"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Project - Core */
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Project - Common */
|
||||
implementation(projects.common.uiCharts)
|
||||
implementation(projects.common.ui)
|
||||
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.models)
|
||||
|
||||
implementation(deps.lifecycle.compose)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.ui.utils)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
}
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
package com.tangem.common.ui.markets
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import com.tangem.common.ui.charts.MarketChartMini
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.markets.preview.MarketChartListItemPreviewDataProvider
|
||||
import com.tangem.common.ui.tokens.TokenPriceText
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.currency.icon.CoinIcon
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.MarketsTestTags
|
||||
import com.tangem.core.ui.windowsize.WindowSizeType
|
||||
import com.tangem.utils.StringsSigns.MINUS
|
||||
import kotlin.random.Random
|
||||
|
||||
@Composable
|
||||
fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
|
||||
MarketsListItemContent(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RectangleShape)
|
||||
.clickable(onClick = onClick)
|
||||
.testTag(MarketsTestTags.TOKENS_LIST_ITEM),
|
||||
model = model,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketsListItemContent(model: MarketsListItemUM, modifier: Modifier = Modifier) {
|
||||
val windowSize = LocalWindowSize.current
|
||||
|
||||
Row(
|
||||
modifier = modifier.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing15,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CoinIcon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size36),
|
||||
url = model.iconUrl,
|
||||
alpha = 1f,
|
||||
colorFilter = null,
|
||||
fallbackResId = R.drawable.ic_custom_token_44,
|
||||
)
|
||||
|
||||
SpacerW12()
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
TokenTitle(
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
name = model.name,
|
||||
currencySymbol = model.currencySymbol,
|
||||
)
|
||||
SpacerW8()
|
||||
TokenPriceText(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
price = model.price.text,
|
||||
priceChangeType = model.price.changeType,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(height = TangemTheme.dimens.spacing2)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
TokenSubtitle(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.alignByBaseline(),
|
||||
ratingPosition = model.ratingPosition,
|
||||
marketCap = model.marketCap,
|
||||
stakingRate = model.stakingRate,
|
||||
)
|
||||
PriceChangeInPercent(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
textStyle = TangemTheme.typography.caption2,
|
||||
type = model.trendType,
|
||||
valueInPercent = model.trendPercentText,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (windowSize.widthAtLeast(WindowSizeType.Small)) {
|
||||
Spacer(Modifier.width(TangemTheme.dimens.spacing10))
|
||||
|
||||
Chart(
|
||||
chartType = model.chartType,
|
||||
chartRawData = model.chartData,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenTitle(name: String, currencySymbol: String, modifier: Modifier = Modifier) {
|
||||
Row(modifier = modifier) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.alignByBaseline(),
|
||||
text = name,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
SpacerW4()
|
||||
Text(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
text = currencySymbol,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Visible,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenSubtitle(
|
||||
ratingPosition: String?,
|
||||
marketCap: String?,
|
||||
stakingRate: TextReference?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TokenRatingPlace(ratingPosition = ratingPosition)
|
||||
if (marketCap != null) {
|
||||
SpacerW4()
|
||||
TokenMarketCapText(
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
text = marketCap,
|
||||
)
|
||||
}
|
||||
if (stakingRate != null) {
|
||||
SpacerW4()
|
||||
StakingRate(stakingRate = stakingRate.resolveReference())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.TokenRatingPlace(ratingPosition: String?) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.alignByBaseline()
|
||||
.heightIn(min = TangemTheme.dimens.size16)
|
||||
.background(
|
||||
color = TangemTheme.colors.field.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersSmall2,
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing5),
|
||||
) {
|
||||
Text(
|
||||
text = ratingPosition ?: MINUS,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.StakingRate(stakingRate: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.alignByBaseline()
|
||||
.heightIn(min = TangemTheme.dimens.size16)
|
||||
.border(
|
||||
width = TangemTheme.dimens.size1,
|
||||
color = TangemTheme.colors.field.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersSmall2,
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing5),
|
||||
) {
|
||||
Text(
|
||||
text = stakingRate,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.TokenMarketCapText(text: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
modifier = modifier.alignByBaseline(),
|
||||
text = text,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawData?) {
|
||||
val chartWidth = TangemTheme.dimens.size56
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing2)
|
||||
.size(height = TangemTheme.dimens.size24, width = chartWidth),
|
||||
) {
|
||||
if (chartRawData != null) {
|
||||
MarketChartMini(
|
||||
rawData = chartRawData,
|
||||
type = chartType,
|
||||
)
|
||||
} else {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size12)
|
||||
.align(Alignment.Center),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal")
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal night", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(showBackground = true, widthDp = 260, name = "small width")
|
||||
@Composable
|
||||
private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::class) state: MarketsListItemUM) {
|
||||
TangemThemePreview {
|
||||
var state1 by remember { mutableStateOf(state) }
|
||||
var state2 by remember { mutableStateOf(state) }
|
||||
var prices by remember {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
100 to PriceChangeType.NEUTRAL,
|
||||
200 to PriceChangeType.NEUTRAL,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
|
||||
MarketsListItem(
|
||||
modifier = Modifier,
|
||||
model = state1,
|
||||
)
|
||||
MarketsListItem(
|
||||
modifier = Modifier,
|
||||
model = state2,
|
||||
)
|
||||
Row {
|
||||
Button(
|
||||
onClick = {
|
||||
state1 = state1.copy(
|
||||
trendType = PriceChangeType.entries.random(),
|
||||
)
|
||||
state2 = state2.copy(
|
||||
trendType = PriceChangeType.entries.random(),
|
||||
)
|
||||
},
|
||||
) { Text(text = "trend") }
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
prices = prices.map { (price, _) ->
|
||||
if (Random.nextBoolean()) {
|
||||
price.inc() to PriceChangeType.UP
|
||||
} else {
|
||||
price.dec() to PriceChangeType.DOWN
|
||||
}
|
||||
}
|
||||
state1 = state1.copy(
|
||||
price = MarketsListItemUM.Price(
|
||||
text = "0.${prices[0].first}023 $",
|
||||
changeType = prices[0].second,
|
||||
),
|
||||
)
|
||||
state2 = state2.copy(
|
||||
price = MarketsListItemUM.Price(
|
||||
text = "0.${prices[1].first}023 $",
|
||||
changeType = prices[1].second,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { Text(text = "price") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package com.tangem.common.ui.markets
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW12
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.windowsize.WindowSizeType
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun MarketsListItemPlaceholder() {
|
||||
val density = LocalDensity.current
|
||||
val windowSize = LocalWindowSize.current
|
||||
val sp12 = with(density) { 12.sp.toDp() }
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing15,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CircleShimmer(Modifier.size(TangemTheme.dimens.size36))
|
||||
|
||||
SpacerW12()
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = TangemTheme.dimens.spacing4),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.width(TangemTheme.dimens.size70)
|
||||
.height(sp12),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(height = TangemTheme.dimens.spacing2)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = TangemTheme.dimens.spacing2),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.width(TangemTheme.dimens.size52)
|
||||
.height(sp12),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (windowSize.widthAtLeast(WindowSizeType.Small)) {
|
||||
Spacer(Modifier.width(TangemTheme.dimens.spacing10))
|
||||
|
||||
Box {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.width(TangemTheme.dimens.size56)
|
||||
.height(TangemTheme.dimens.size12),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal")
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal night", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(showBackground = true, widthDp = 320, name = "small width")
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
Column(Modifier.background(TangemTheme.colors.background.tertiary)) {
|
||||
repeat(20) {
|
||||
MarketsListItemPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.common.ui.markets.models
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
||||
@Immutable
|
||||
data class MarketsListItemUM(
|
||||
val id: CryptoCurrency.RawID,
|
||||
val name: String,
|
||||
val currencySymbol: String,
|
||||
val iconUrl: String?,
|
||||
val ratingPosition: String?,
|
||||
val marketCap: String?,
|
||||
val price: Price,
|
||||
val trendPercentText: String,
|
||||
val trendType: PriceChangeType,
|
||||
val chartData: MarketChartRawData?,
|
||||
val isUnder100kMarketCap: Boolean,
|
||||
val stakingRate: TextReference?,
|
||||
val updateTimestamp: Long?,
|
||||
) {
|
||||
val chartType: MarketChartLook.Type = when (trendType) {
|
||||
PriceChangeType.UP -> MarketChartLook.Type.Growing
|
||||
PriceChangeType.DOWN -> MarketChartLook.Type.Falling
|
||||
PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class Price(
|
||||
val text: String,
|
||||
val changeType: PriceChangeType? = null,
|
||||
)
|
||||
|
||||
@Suppress("NullableToStringCall")
|
||||
fun getComposeKey(): String {
|
||||
return id.value + TOKEN_LAZY_LIST_ID_SEPARATOR + marketCap.toString() + updateTimestamp
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TOKEN_LAZY_LIST_ID_SEPARATOR = "@"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
package com.tangem.common.ui.markets.preview
|
||||
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider<MarketsListItemUM>(
|
||||
collection = listOf(
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = "",
|
||||
ratingPosition = "10",
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chartData = MarketChartRawData(
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
),
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = "10",
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.NEUTRAL,
|
||||
chartData = null,
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
),
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = "10",
|
||||
marketCap = "$6.23348172384781234 B",
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.DOWN,
|
||||
chartData = MarketChartRawData(
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
),
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = "10",
|
||||
marketCap = null,
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chartData = MarketChartRawData(
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
),
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = null,
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chartData = MarketChartRawData(
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
),
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = null,
|
||||
marketCap = null,
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chartData = MarketChartRawData(
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -18,9 +18,7 @@
|
|||
<ID>CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$minimumSendAmount: BigDecimal?</ID>
|
||||
<ID>CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$rentWarning: CryptoCurrencyWarning.Rent?</ID>
|
||||
<ID>MultilineLambdaItParameter:ExpressStatusItems.kt${ val itemInfo = expressTxs[it].info val (iconRes, tint) = when (itemInfo.iconState) { ExpressTransactionStateIconUM.Warning -> { R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention } ExpressTransactionStateIconUM.Error -> { R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning } ExpressTransactionStateIconUM.None -> null to null } ExpressStatusItem( title = itemInfo.title, fromTokenIconState = itemInfo.fromCurrencyIcon, toTokenIconState = itemInfo.toCurrencyIcon, fromAmount = itemInfo.fromAmount, fromSymbol = itemInfo.fromAmountSymbol, toAmount = itemInfo.toAmount, toSymbol = itemInfo.toAmountSymbol, onClick = itemInfo.onClick, infoIconRes = iconRes, infoIconTint = tint, modifier = modifier.animateItem(), ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:TokenItemStateConverter.kt$TokenItemStateConverter${ createTitleState(it, yieldModuleApyMap, stakingApyMap, { onApyLabelClick?.invoke(it) }) }</ID>
|
||||
<ID>MultilineLambdaItParameter:TokenItemStateConverter.kt$TokenItemStateConverter.Companion${ it.key.equals( other = token.yieldSupplyKey(), ignoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId), ) }</ID>
|
||||
<ID>NamedArguments:TokenItemStateConverter.kt$TokenItemStateConverter$createTitleState(it, yieldModuleApyMap, stakingApyMap, { onApyLabelClick?.invoke(it) })</ID>
|
||||
<ID>NoNameShadowing:NavigationButtonsBlock.kt$navigationUM</ID>
|
||||
<ID>NoNameShadowing:UserWalletItem.kt$balance</ID>
|
||||
<ID>NullableBooleanCheck:TokenItemStateConverter.kt$TokenItemStateConverter.Companion$cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive ?: false</ID>
|
||||
|
|
|
|||
|
|
@ -271,6 +271,7 @@ private fun AmountFieldError(
|
|||
style = TangemTheme.typography.caption2,
|
||||
color = color,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.testTag(SendScreenTestTags.AMOUNT_ERROR_TEXT),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
package com.tangem.common.ui.news
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
@Composable
|
||||
internal fun ArticleBadge(articleTagUM: ArticleTagUM, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = modifier
|
||||
.heightIn(min = 24.dp)
|
||||
.background(
|
||||
color = TangemTheme.colors.icon.informative.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
) {
|
||||
when (articleTagUM) {
|
||||
is ArticleTagUM.Category -> Unit
|
||||
is ArticleTagUM.Token -> {
|
||||
CurrencyIcon(
|
||||
state = articleTagUM.iconState,
|
||||
shouldDisplayNetwork = false,
|
||||
iconSize = 16.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = articleTagUM.title.resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ArticleBadgePreview() {
|
||||
TangemThemePreview {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
ArticleBadge(
|
||||
articleTagUM = ArticleTagUM.Token(
|
||||
TextReference.Str("BTC"),
|
||||
iconState = CurrencyIconState.CoinIcon(
|
||||
url = "",
|
||||
fallbackResId = 0,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
ArticleBadge(
|
||||
articleTagUM = ArticleTagUM.Category(TextReference.Str("Regulation")),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +1,48 @@
|
|||
package com.tangem.common.ui.news
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.CardColors
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.label.Label
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.utils.StringsSigns
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@Composable
|
||||
fun ArticleCard(articleConfigUM: ArticleConfigUM, onArticleClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
fun ArticleCard(
|
||||
articleConfigUM: ArticleConfigUM,
|
||||
onArticleClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
colors: CardColors = TangemBlockCardColors,
|
||||
) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
onClick = onArticleClick,
|
||||
colors = colors,
|
||||
) {
|
||||
if (articleConfigUM.isTrending) {
|
||||
TrendingArticle(articleConfigUM = articleConfigUM)
|
||||
|
|
@ -81,7 +88,7 @@ private fun TrendingArticle(articleConfigUM: ArticleConfigUM) {
|
|||
|
||||
ArticleInfo(
|
||||
score = articleConfigUM.score,
|
||||
createdAt = articleConfigUM.createdAt,
|
||||
createdAt = articleConfigUM.createdAt.resolveReference(),
|
||||
)
|
||||
|
||||
SpacerH(32.dp)
|
||||
|
|
@ -98,7 +105,7 @@ private fun DefaultArticle(articleConfigUM: ArticleConfigUM) {
|
|||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
ArticleInfo(
|
||||
score = articleConfigUM.score,
|
||||
createdAt = articleConfigUM.createdAt,
|
||||
createdAt = articleConfigUM.createdAt.resolveReference(),
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
|
@ -121,54 +128,13 @@ private fun DefaultArticle(articleConfigUM: ArticleConfigUM) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = Modifier) {
|
||||
val dotColor = TangemTheme.colors.text.secondary
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_start_circle_12),
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = score.toString(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.size(4.dp)
|
||||
.drawWithCache {
|
||||
val radius = size.minDimension / 2f
|
||||
onDrawBehind {
|
||||
drawCircle(
|
||||
color = dotColor,
|
||||
radius = radius,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Text(
|
||||
text = createdAt,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun Tags(tags: ImmutableList<ArticleTagUM>, modifier: Modifier = Modifier) {
|
||||
private fun Tags(tags: ImmutableList<LabelUM>, modifier: Modifier = Modifier) {
|
||||
val expandIndicator = remember {
|
||||
ContextualFlowRowOverflow.expandIndicator {
|
||||
val remainingItems = tags.size - shownItemCount
|
||||
ArticleBadge(articleTagUM = ArticleTagUM.Category(TextReference.Str("${StringsSigns.PLUS}$remainingItems")))
|
||||
Label(state = LabelUM(TextReference.Str("${StringsSigns.PLUS}$remainingItems")))
|
||||
}
|
||||
}
|
||||
ContextualFlowRow(
|
||||
|
|
@ -179,7 +145,7 @@ private fun Tags(tags: ImmutableList<ArticleTagUM>, modifier: Modifier = Modifie
|
|||
maxLines = 1,
|
||||
overflow = expandIndicator,
|
||||
) { index ->
|
||||
ArticleBadge(articleTagUM = tags[index])
|
||||
Label(state = tags[index])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -189,14 +155,14 @@ private fun Tags(tags: ImmutableList<ArticleTagUM>, modifier: Modifier = Modifie
|
|||
private fun TagsPreview() {
|
||||
TangemThemePreview {
|
||||
Tags(
|
||||
tags = listOf(
|
||||
ArticleTagUM.Category(TextReference.Str("Hype")),
|
||||
ArticleTagUM.Category(TextReference.Str("BTC")),
|
||||
ArticleTagUM.Category(TextReference.Str("Supply")),
|
||||
ArticleTagUM.Category(TextReference.Str("Demand")),
|
||||
ArticleTagUM.Category(TextReference.Str("Best rate")),
|
||||
ArticleTagUM.Category(TextReference.Str("Breaking news")),
|
||||
).toPersistentList(),
|
||||
tags = persistentListOf(
|
||||
LabelUM(TextReference.Str("Hype")),
|
||||
LabelUM(TextReference.Str("BTC")),
|
||||
LabelUM(TextReference.Str("Supply")),
|
||||
LabelUM(TextReference.Str("Demand")),
|
||||
LabelUM(TextReference.Str("Best rate")),
|
||||
LabelUM(TextReference.Str("Breaking news")),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -206,19 +172,18 @@ private fun TagsPreview() {
|
|||
@Composable
|
||||
private fun ArticleCardsPreview() {
|
||||
val tags = listOf(
|
||||
ArticleTagUM.Category(TextReference.Str("Hype")),
|
||||
ArticleTagUM.Category(TextReference.Str("BTC")),
|
||||
ArticleTagUM.Category(TextReference.Str("Supply")),
|
||||
ArticleTagUM.Category(TextReference.Str("Demand")),
|
||||
ArticleTagUM.Category(TextReference.Str("Best rate")),
|
||||
ArticleTagUM.Category(TextReference.Str("Breaking news")),
|
||||
LabelUM(TextReference.Str("Hype")),
|
||||
LabelUM(TextReference.Str("BTC")),
|
||||
LabelUM(TextReference.Str("Supply")),
|
||||
LabelUM(TextReference.Str("Demand")),
|
||||
LabelUM(TextReference.Str("Breaking news")),
|
||||
).toImmutableSet()
|
||||
|
||||
val config = ArticleConfigUM(
|
||||
id = 1,
|
||||
title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)",
|
||||
score = 9.5f,
|
||||
createdAt = "1h ago",
|
||||
createdAt = TextReference.Str("1h ago"),
|
||||
isTrending = true,
|
||||
tags = tags,
|
||||
isViewed = false,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
package com.tangem.common.ui.news
|
||||
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
|
||||
data class ArticleConfigUM(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val score: Float,
|
||||
val createdAt: String,
|
||||
val createdAt: TextReference,
|
||||
val isTrending: Boolean,
|
||||
val tags: ImmutableSet<ArticleTagUM>,
|
||||
val tags: ImmutableSet<LabelUM>,
|
||||
val isViewed: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.common.ui.news
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.label.Label
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ArticleHeader(
|
||||
title: String,
|
||||
createdAt: String,
|
||||
score: Float,
|
||||
tags: ImmutableList<LabelUM>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
ArticleInfo(
|
||||
score = score,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
if (tags.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
tags.forEach { tag ->
|
||||
Label(
|
||||
state = tag,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.common.ui.news
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.drawWithCache
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
internal fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = Modifier) {
|
||||
val dotColor = TangemTheme.colors.text.secondary
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_start_circle_12),
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = score.toString(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.size(4.dp)
|
||||
.drawWithCache {
|
||||
val radius = size.minDimension / 2f
|
||||
onDrawBehind {
|
||||
drawCircle(
|
||||
color = dotColor,
|
||||
radius = radius,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Text(
|
||||
text = createdAt,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.tangem.common.ui.news
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
fun TrendingLoadingArticle(modifier: Modifier = Modifier) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 24.dp, horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 96.dp, height = 24.dp), radius = 8.dp)
|
||||
SpacerH(12.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 285.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(6.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 190.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(14.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(32.dp)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DefaultLoadingArticle() {
|
||||
BlockCard(
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 16.dp), radius = 4.dp)
|
||||
SpacerH(12.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 142.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(6.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 176.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(6.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 120.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(16.dp)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
package com.tangem.common.ui.news
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
sealed interface ArticleTagUM {
|
||||
|
||||
val title: TextReference
|
||||
|
||||
data class Category(override val title: TextReference) : ArticleTagUM
|
||||
|
||||
data class Token(
|
||||
override val title: TextReference,
|
||||
val iconState: CurrencyIconState,
|
||||
) : ArticleTagUM
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ import com.tangem.domain.models.StatusSource
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.currency.yieldSupplyKey
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.StakingOption
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
|
|
@ -171,7 +171,7 @@ class TokenItemStateConverter(
|
|||
return totalAmount.format { crypto(currency) }
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.getStakedBalance() = (value.yieldBalance as? YieldBalance.Data)
|
||||
private fun CryptoCurrencyStatus.getStakedBalance() = (value.stakingBalance as? StakingBalance.Data)
|
||||
?.getTotalWithRewardsStakingBalance(blockchainId = currency.network.rawId).orZero()
|
||||
|
||||
private fun createTitleState(
|
||||
|
|
@ -277,14 +277,18 @@ class TokenItemStateConverter(
|
|||
val stakingAvailability = stakingApyMap[currencyStatus.currency] as? StakingAvailability.Available
|
||||
?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null)
|
||||
|
||||
val yieldBalance = currencyStatus.value.yieldBalance
|
||||
val hasStakedBalance = yieldBalance is YieldBalance.Data
|
||||
val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data
|
||||
val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit
|
||||
|
||||
val rateInfo = when (val stakingOptions = stakingAvailability.option) {
|
||||
is StakingOption.P2P -> null // todo p2p
|
||||
is StakingOption.StakeKit -> if (hasStakedBalance) {
|
||||
is StakingOption.P2P -> {
|
||||
// P2P or no balance: use preferred validators
|
||||
// TODO add p2p logic
|
||||
null
|
||||
}
|
||||
is StakingOption.StakeKit -> if (stakeKitBalance != null) {
|
||||
val validatorsByAddress = stakingOptions.yield.validators.associateBy { it.address }
|
||||
yieldBalance.balance.items
|
||||
stakeKitBalance.balance.items
|
||||
.mapNotNull { it.validatorAddress }
|
||||
.firstNotNullOfOrNull { address ->
|
||||
validatorsByAddress[address]?.rewardInfo
|
||||
|
|
@ -306,7 +310,7 @@ class TokenItemStateConverter(
|
|||
|
||||
return StakingLocalInfo(
|
||||
rate = rateInfo?.rate,
|
||||
isActive = hasStakedBalance,
|
||||
isActive = stakeKitBalance != null, // todo add p2p check
|
||||
rewardType = rateInfo?.type,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.common.ui.userwallet
|
||||
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.event.SignIn
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
|
|
@ -13,6 +15,8 @@ import com.tangem.domain.common.wallets.error.UnlockWalletError.UnableToUnlock.R
|
|||
inline fun UnlockWalletError.handle(
|
||||
onAlreadyUnlocked: () -> Unit = {},
|
||||
onUserCancelled: () -> Unit = {},
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
isFromUnlockAll: Boolean,
|
||||
noinline showMessage: (EventMessage) -> Unit,
|
||||
) {
|
||||
when (this) {
|
||||
|
|
@ -30,15 +34,26 @@ inline fun UnlockWalletError.handle(
|
|||
// This should never happen in this flow, as we always check for the wallet existence before unlocking
|
||||
showMessage(SnackbarMessage(TextReference.Res(R.string.generic_error)))
|
||||
}
|
||||
is UnlockWalletError.UnableToUnlock -> handleUnableToUnlock(this, showMessage)
|
||||
is UnlockWalletError.UnableToUnlock -> handleUnableToUnlock(
|
||||
isFromUnlockAll = isFromUnlockAll,
|
||||
error = this,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
showDialog = showMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleUnableToUnlock(error: UnlockWalletError.UnableToUnlock, showDialog: (DialogMessage) -> Unit) {
|
||||
fun handleUnableToUnlock(
|
||||
isFromUnlockAll: Boolean,
|
||||
error: UnlockWalletError.UnableToUnlock,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
showDialog: (DialogMessage) -> Unit,
|
||||
) {
|
||||
val dialogMessage = when (error) {
|
||||
is UnlockWalletError.UnableToUnlock.WithReason -> {
|
||||
when (error.reason) {
|
||||
Reason.AllKeysInvalidated -> {
|
||||
analyticsEventHandler.send(SignIn.ErrorBiometricUpdated(isFromUnlockAll))
|
||||
DialogMessage(
|
||||
title = resourceReference(R.string.biometric_updated_warning_title),
|
||||
message = resourceReference(R.string.biometric_updated_warning_description),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue