Updated on 2026-08-14

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

View file

@ -142,6 +142,8 @@ dependencies {
implementation(projects.features.pushNotifications.impl)
implementation(projects.features.walletSettings.api)
implementation(projects.features.walletSettings.impl)
implementation(projects.features.markets.api)
implementation(projects.features.markets.impl)
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)

View file

@ -70,7 +70,7 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh
)
Text(
text = stringResource(
id = R.string.give_permission_subtitle,
id = R.string.give_permission_swap_subtitle,
data.currency,
),
color = TangemTheme.colors.text.secondary,

View file

@ -40,15 +40,7 @@ inline fun <reified M : Model, reified P : Any> AppComponentContext.getOrCreateM
val hiltComponent = hiltComponentBuilder
.router(router)
.uiMessageSender(messageSender)
.let { builder ->
if (params != null) {
val container = MutableParamsContainer(params)
builder.paramsContainer(container)
} else {
builder
}
}
.paramsContainer(MutableParamsContainer(params ?: Unit))
.build()
EntryPoints.get(hiltComponent, ModelsEntryPoint::class.java)

View file

@ -42,5 +42,9 @@
{
"name": "PUSH_NOTIFICATIONS_ENABLED",
"version": "5.13.0"
},
{
"name": "MARKETS_ENABLED",
"version": "undefined"
}
]

View file

@ -268,7 +268,8 @@
<string name="give_permission_fee_footer">Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена.</string>
<string name="give_permission_policy_type_footer">Укажите лимит доступа к выбранному токену</string>
<string name="give_permission_rows_amount">Количество %s</string>
<string name="give_permission_subtitle">Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s</string>
<string name="give_permission_staking_footer">Функция подтверждения необходима для предоставления другому адресу разрешения на использование определенного количества ваших токенов.По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту StakeKit разрешение использовать ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете осуществить стейкинг токена.</string>
<string name="give_permission_swap_subtitle" formatted="false">Чтобы продолжить, вам нужно разрешить смарт-контракту %1s использовать ваш %2s</string>
<string name="give_permission_title">Дать разрешение</string>
<string name="give_permission_unlimited">Безлимитно</string>
<string name="home_button_order">Купить</string>

View file

@ -271,7 +271,9 @@
<string name="give_permission_fee_footer">The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap.</string>
<string name="give_permission_policy_type_footer">Specify the approve limit for the selected token</string>
<string name="give_permission_rows_amount">Amount %s</string>
<string name="give_permission_subtitle">To continue, grant 1inch smart contracts permission to use your %s</string>
<string name="give_permission_staking_footer">The Approve function is needed to grant permission to another address to use a specific amount of your tokens. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the StakeKit smart contract to use them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can stake your token after giving approval.</string>
<string name="give_permission_staking_subtitle">To continue you need to allow StakeKit smart contract to use your %s</string>
<string name="give_permission_swap_subtitle" formatted="false">To continue, grant %1s smart contracts permission to use your %2s</string>
<string name="give_permission_title">Give Permission</string>
<string name="give_permission_unlimited">Unlimited</string>
<string name="home_button_order">Order card</string>
@ -327,15 +329,62 @@
<string name="markets_add_to_my_portfolio_description">To start buying, exchanging or receiving this asset, add this token to at least 1 network</string>
<string name="markets_add_to_my_portfolio_unavailable_description">This asset is not available</string>
<string name="markets_add_to_portfolio_button">Add to portfolio</string>
<string name="markets_available_networks">Available networks</string>
<string name="markets_common_my_portfolio">My portfolio</string>
<string name="markets_common_title">Market</string>
<string name="markets_select_network">Select network</string>
<string name="markets_select_wallet">Select wallet</string>
<string name="markets_selector_interval_1m_title">1m</string>
<string name="markets_selector_interval_1y_title">1y</string>
<string name="markets_selector_interval_24h_title">24h</string>
<string name="markets_selector_interval_3m_title">3m</string>
<string name="markets_selector_interval_6m_title">6m</string>
<string name="markets_selector_interval_7d_title">7d</string>
<string name="markets_selector_interval_all_title">All</string>
<string name="markets_sort_by_experienced_buyers_title">Experienced buyers</string>
<string name="markets_sort_by_rating_title">Rating</string>
<string name="markets_sort_by_title">Sort By</string>
<string name="markets_sort_by_top_gainers_title">Top Gainers</string>
<string name="markets_sort_by_top_losers_title">Top losers</string>
<string name="markets_sort_by_trending_title">Trending</string>
<plurals name="markets_token_details_based_on_ratings">
<item quantity="one">Based on %d rating</item>
<item quantity="other">Based on %d ratings</item>
</plurals>
<string name="markets_token_details_blockchain_site">Blockchain site</string>
<string name="markets_token_details_buy_pressure">Buy pressure</string>
<string name="markets_token_details_buy_pressure_description">The difference between buyers volume and sellers volume</string>
<string name="markets_token_details_circulating_supply">Circulating supply</string>
<string name="markets_token_details_circulating_supply_description">The total number of coins that are available for trading and are circulating in the market</string>
<string name="markets_token_details_experienced_buyers">Experienced buyers</string>
<string name="markets_token_details_experienced_buyers_description">Net buyers with the additional requirement of having at least 100 outgoing transactions</string>
<string name="markets_token_details_fully_diluted_valuation">Fully diluted valuation</string>
<string name="markets_token_details_fully_diluted_valuation_description">The total theoretical value of a cryptocurrency if all coins that could exist are in circulation, including those not currently circulating</string>
<string name="markets_token_details_genesis_date">Genesis date</string>
<string name="markets_token_details_high">High</string>
<string name="markets_token_details_holders">Holders</string>
<string name="markets_token_details_holders_description">The change in the number of token holders within a specific timeframe</string>
<string name="markets_token_details_insights">Insights</string>
<string name="markets_token_details_links">Links</string>
<string name="markets_token_details_liquidity">Liquidity</string>
<string name="markets_token_details_liquidity_description">The change in how much liquidity is available for the token during the specified timeframe</string>
<string name="markets_token_details_liquidity_index">Liquidity index</string>
<string name="markets_token_details_low">Low</string>
<string name="markets_token_details_market_capitalization">Market capitalization</string>
<string name="markets_token_details_market_capitalization_description">The total market value of a cryptocurrency, calculated by multiplying the current price of the coin by the total number of coins in circulation</string>
<string name="markets_token_details_market_rating">Market rating</string>
<string name="markets_token_details_market_rating_description">Position in crypto rating between all coins based on market capitalization</string>
<string name="markets_token_details_max_supply">Max supply</string>
<string name="markets_token_details_metrics">Metrics</string>
<string name="markets_token_details_official_links">Official links</string>
<string name="markets_token_details_price_performance">Price performance</string>
<string name="markets_token_details_repository">Repository</string>
<string name="markets_token_details_security_score">Security score</string>
<string name="markets_token_details_social">Social</string>
<string name="markets_token_details_total_supply">Total supply</string>
<string name="markets_token_details_total_supply_description">The maximum number of coins or tokens that can ever exist for a particular cryptocurrency</string>
<string name="markets_token_details_trading_volume">Trading volume (24h)</string>
<string name="markets_token_details_trading_volume_24h_description">The total amount of a cryptocurrency that has been traded within the last 24 hours, indicating the level of activity and liquidity in the market</string>
<string name="onboarding_access_code_feature_1_description">You have to set up a single access code to protect all your cards</string>
<string name="onboarding_access_code_feature_1_title">Protect</string>
<string name="onboarding_access_code_feature_2_description">You can set up an individual access code on each card later</string>
@ -675,7 +724,7 @@
<string name="twins_scan_twin_with_number">Tap the twin card with number %s and do not remove until the end of the operation</string>
<string name="unlock_wallet_description_full">Use %s or scan a card to have an access to your wallet</string>
<string name="user_push_notification_agreement_argument_one">Stay up to date with the latest features and news</string>
<string name="user_push_notification_agreement_argument_two">Be able to get important market notifications</string>
<string name="user_push_notification_agreement_argument_two">Be the first to know about new promotions</string>
<string name="user_push_notification_agreement_header">Would you like to use\nPush-notifications?</string>
<string name="user_wallet_list_add_button">Add new wallet</string>
<string name="user_wallet_list_delete_prompt">Are you sure you want to delete this wallet?</string>

View file

@ -17,10 +17,7 @@ import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.res.LocalIsInDarkTheme
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.*
import com.valentinilk.shimmer.*
/**
@ -31,7 +28,7 @@ fun RectangleShimmer(modifier: Modifier = Modifier, radius: Dp = TangemTheme.dim
Box(
modifier = modifier
.clip(RoundedCornerShape(size = radius))
.shimmer(TangemShimmer),
.shimmer(LocalTangemShimmer.current),
)
}
@ -44,11 +41,11 @@ fun CircleShimmer(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.clip(CircleShape)
.shimmer(TangemShimmer),
.shimmer(LocalTangemShimmer.current),
)
}
private val TangemShimmer: Shimmer
internal val TangemShimmer: Shimmer
@Composable
get() = rememberShimmer(
shimmerBounds = ShimmerBounds.View,

View file

@ -4,9 +4,7 @@ import android.content.res.Configuration
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@ -18,6 +16,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
@ -57,12 +56,14 @@ inline fun <reified T> SegmentedButtons(
val index = if (initialSelectedItem == null) 0 else config.indexOf(initialSelectedItem)
mutableIntStateOf(index)
}
val shape = RoundedCornerShape(TangemTheme.dimens.radius26)
Row(
modifier = modifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius26))
.clip(shape)
.background(dividerColor)
.padding(TangemTheme.dimens.spacing1),
.border(BorderStroke(1.dp, dividerColor), shape = shape)
.height(IntrinsicSize.Max),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing1),
) {
repeat(config.size) { index ->

View file

@ -2,7 +2,7 @@ package com.tangem.core.ui.components.fields
import android.content.res.Configuration
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@ -37,7 +37,7 @@ fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFie
TextField(
modifier = modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size48)
.heightIn(min = TangemTheme.dimens.size48)
.onFocusChanged {
if (it.isFocused) {
state.onActiveChange(true)

View file

@ -8,6 +8,5 @@ import androidx.compose.ui.Modifier
fun interface ComposableContentComponent {
@Composable
@Suppress("TopLevelComposableFunctions") // TODO: Remove this check
fun Content(modifier: Modifier)
}

View file

@ -9,6 +9,5 @@ interface ComposableDialogComponent {
val doOnDismiss: () -> Unit
@Composable
@Suppress("TopLevelComposableFunctions") // TODO: Remove this check
fun Dialog()
}

View file

@ -7,9 +7,11 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.components.TangemShimmer
import com.tangem.core.ui.haptic.HapticManager
import com.tangem.core.ui.haptic.MockHapticManager
import com.tangem.core.ui.windowsize.WindowSize
import com.valentinilk.shimmer.Shimmer
@Composable
fun TangemTheme(
@ -48,6 +50,9 @@ fun TangemTheme(
LocalHapticManager provides hapticManager,
LocalSnackbarHostState provides snackbarHostState,
LocalWindowSize provides windowSize,
) {
CompositionLocalProvider(
LocalTangemShimmer provides TangemShimmer,
) {
ProvideTextStyle(
value = TangemTheme.typography.body1,
@ -55,6 +60,7 @@ fun TangemTheme(
)
}
}
}
}
object TangemTheme {
@ -228,3 +234,7 @@ val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
val LocalWindowSize = staticCompositionLocalOf<WindowSize> {
error("No WindowSize provided")
}
val LocalTangemShimmer = staticCompositionLocalOf<Shimmer> {
error("No TangemShimmer provided")
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

@ -1 +1 @@
Subproject commit d8e6da9ab3a0442657822352cb6274237d88e77d
Subproject commit 0ed07b85e64805707b2ef6a92f42bc2c4e8b7753