From 71038ca2d866ea9c2ee13812b82e816a376c2932 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Jul 2024 14:49:08 +0300 Subject: [PATCH] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../GiveTxPermisssionBottomSheet.kt | 2 +- .../core/decompose/model/ModelEntryPoint.kt | 10 +- .../configs/feature_toggles_config.json | 4 + core/res/src/main/res/values-ru/strings.xml | 3 +- core/res/src/main/res/values/strings.xml | 53 ++- .../com/tangem/core/ui/components/Shimmers.kt | 11 +- .../segmentedbutton/SegmentedButton.kt | 11 +- .../core/ui/components/fields/SearchBar.kt | 4 +- .../decompose/ComposableContentComponent.kt | 1 - .../ui/decompose/ComposableDialogComponent.kt | 1 - .../com/tangem/core/ui/res/TangemTheme.kt | 18 +- .../preview/PreviewDetailsComponent.kt | 1 - .../preview/PreviewUserWalletListComponent.kt | 1 - .../features/markets/MarketsFeatureToggles.kt | 5 + .../features/markets/MarketsListComponent.kt | 12 - .../{ => component}/BottomSheetState.kt | 2 +- .../markets/component/MarketsListComponent.kt | 23 ++ features/markets/impl/build.gradle.kts | 18 + .../markets/DefaultMarketsFeatureToggles.kt | 11 + .../impl/DefaultMarketsListComponent.kt | 44 +++ .../features/markets/di/ComponentModule.kt | 18 + .../features/markets/di/FeatureModule.kt | 20 ++ .../tangem/features/markets/di/ModelModule.kt | 20 ++ .../markets/model/MarketsListModel.kt | 61 ++++ .../tangem/features/markets/ui/MarketsList.kt | 217 ++++++++++++ .../ui/{ => components}/MarketsListItem.kt | 19 +- .../components/MarketsListItemPlaceholder.kt | 110 ++++++ .../features/markets/ui/entity/ListItem.kt | 3 + .../MarketsListItemUM.kt} | 4 +- .../markets/ui/entity/MarketsListUM.kt | 42 +++ .../MarketChartListItemPreviewDataProvider.kt | 28 +- .../tangem/feature/swap/ui/StateBuilder.kt | 2 +- features/wallet/impl/build.gradle.kts | 1 + .../wallet/presentation/WalletFragment.kt | 37 ++ .../router/DefaultWalletRouter.kt | 4 +- .../presentation/router/InnerWalletRouter.kt | 4 +- .../presentation/wallet/ui/WalletScreen.kt | 333 +++++++++++++++++- tangem-android-tools | 2 +- 39 files changed, 1073 insertions(+), 89 deletions(-) create mode 100644 features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsFeatureToggles.kt delete mode 100644 features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsListComponent.kt rename features/markets/api/src/main/kotlin/com/tangem/features/markets/{ => component}/BottomSheetState.kt (57%) create mode 100644 features/markets/api/src/main/kotlin/com/tangem/features/markets/component/MarketsListComponent.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsFeatureToggles.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ComponentModule.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/FeatureModule.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ModelModule.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/{ => components}/MarketsListItem.kt (96%) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItemPlaceholder.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/ListItem.kt rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/{models/MarketsListItemModel.kt => entity/MarketsListItemUM.kt} (91%) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c6f66b3c04..c58fa8b592 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt index e65b7b86f1..0922cb2cc6 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt @@ -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, diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt index 286678b4c3..56b718e970 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt @@ -40,15 +40,7 @@ inline fun 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) diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index 1a01fb3200..7170767402 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -42,5 +42,9 @@ { "name": "PUSH_NOTIFICATIONS_ENABLED", "version": "5.13.0" + }, + { + "name": "MARKETS_ENABLED", + "version": "undefined" } ] diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 015132e02d..6cb64bafa2 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -268,7 +268,8 @@ Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена. Укажите лимит доступа к выбранному токену Количество %s - Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s + Функция подтверждения необходима для предоставления другому адресу разрешения на использование определенного количества ваших токенов.По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту StakeKit разрешение использовать ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете осуществить стейкинг токена. + Чтобы продолжить, вам нужно разрешить смарт-контракту %1s использовать ваш %2s Дать разрешение Безлимитно Купить diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b38d48131f..4a8d97d929 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -271,7 +271,9 @@ The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. Specify the approve limit for the selected token Amount %s - To continue, grant 1inch smart contracts permission to use your %s + 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. + To continue you need to allow StakeKit smart contract to use your %s + To continue, grant %1s smart contracts permission to use your %2s Give Permission Unlimited Order card @@ -327,15 +329,62 @@ To start buying, exchanging or receiving this asset, add this token to at least 1 network This asset is not available Add to portfolio + Available networks My portfolio Market + Select network Select wallet + 1m + 1y + 24h + 3m + 6m + 7d + All + Experienced buyers + Rating Sort By + Top Gainers + Top losers + Trending + + Based on %d rating + Based on %d ratings + + Blockchain site + Buy pressure + The difference between buyers volume and sellers volume + Circulating supply + The total number of coins that are available for trading and are circulating in the market + Experienced buyers + Net buyers with the additional requirement of having at least 100 outgoing transactions + Fully diluted valuation + The total theoretical value of a cryptocurrency if all coins that could exist are in circulation, including those not currently circulating + Genesis date + High + Holders + The change in the number of token holders within a specific timeframe Insights Links + Liquidity + The change in how much liquidity is available for the token during the specified timeframe + Liquidity index + Low + Market capitalization + The total market value of a cryptocurrency, calculated by multiplying the current price of the coin by the total number of coins in circulation + Market rating + Position in crypto rating between all coins based on market capitalization + Max supply Metrics + Official links Price performance + Repository Security score + Social + Total supply + The maximum number of coins or tokens that can ever exist for a particular cryptocurrency + Trading volume (24h) + 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 You have to set up a single access code to protect all your cards Protect You can set up an individual access code on each card later @@ -675,7 +724,7 @@ Tap the twin card with number %s and do not remove until the end of the operation Use %s or scan a card to have an access to your wallet Stay up to date with the latest features and news - Be able to get important market notifications + Be the first to know about new promotions Would you like to use\nPush-notifications? Add new wallet Are you sure you want to delete this wallet? diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt index ddb5122ee1..2e5b4ee1c2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt @@ -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, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt index 1101935641..c4be2dacd0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt @@ -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 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 -> diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt index 0c669a23ad..cf2abf3f5f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt @@ -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) diff --git a/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableContentComponent.kt b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableContentComponent.kt index 4f8144398d..c44405f037 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableContentComponent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableContentComponent.kt @@ -8,6 +8,5 @@ import androidx.compose.ui.Modifier fun interface ComposableContentComponent { @Composable - @Suppress("TopLevelComposableFunctions") // TODO: Remove this check fun Content(modifier: Modifier) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableDialogComponent.kt b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableDialogComponent.kt index a827872ee3..24eed9ce86 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableDialogComponent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableDialogComponent.kt @@ -9,6 +9,5 @@ interface ComposableDialogComponent { val doOnDismiss: () -> Unit @Composable - @Suppress("TopLevelComposableFunctions") // TODO: Remove this check fun Dialog() } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index ea874c3bfb..1bd2251a68 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -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( @@ -49,10 +51,14 @@ fun TangemTheme( LocalSnackbarHostState provides snackbarHostState, LocalWindowSize provides windowSize, ) { - ProvideTextStyle( - value = TangemTheme.typography.body1, - content = content, - ) + CompositionLocalProvider( + LocalTangemShimmer provides TangemShimmer, + ) { + ProvideTextStyle( + value = TangemTheme.typography.body1, + content = content, + ) + } } } } @@ -227,4 +233,8 @@ val LocalSnackbarHostState = staticCompositionLocalOf { val LocalWindowSize = staticCompositionLocalOf { error("No WindowSize provided") +} + +val LocalTangemShimmer = staticCompositionLocalOf { + error("No TangemShimmer provided") } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index 5d2d2b67d0..209cad1288 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -33,7 +33,6 @@ internal class PreviewDetailsComponent : DetailsComponent { ) @Composable - @Suppress("TopLevelComposableFunctions") // TODO: Remove this check override fun Content(modifier: Modifier) { DetailsScreen( modifier = modifier, diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt index 09950611e8..a8906b5061 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt @@ -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) } diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsFeatureToggles.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsFeatureToggles.kt new file mode 100644 index 0000000000..34758a7b41 --- /dev/null +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.markets + +interface MarketsFeatureToggles { + val isFeatureEnabled: Boolean +} \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsListComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsListComponent.kt deleted file mode 100644 index e987e0d66e..0000000000 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsListComponent.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.features.markets - -import androidx.compose.runtime.Stable - -@Stable -interface MarketsListComponent { - - // TODO - - // @Composable - // fun BottomSheetContent(state: State, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier) -} \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/BottomSheetState.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/component/BottomSheetState.kt similarity index 57% rename from features/markets/api/src/main/kotlin/com/tangem/features/markets/BottomSheetState.kt rename to features/markets/api/src/main/kotlin/com/tangem/features/markets/component/BottomSheetState.kt index 0084b73528..5c8cc47920 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/BottomSheetState.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/component/BottomSheetState.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets +package com.tangem.features.markets.component enum class BottomSheetState { EXPANDED, diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/component/MarketsListComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/component/MarketsListComponent.kt new file mode 100644 index 0000000000..c0d14dbea5 --- /dev/null +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/component/MarketsListComponent.kt @@ -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, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier, + ) + + interface Factory { + fun create(context: AppComponentContext): MarketsListComponent + } +} \ No newline at end of file diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 80288a27be..86ae97c792 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -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) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsFeatureToggles.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsFeatureToggles.kt new file mode 100644 index 0000000000..75062eddef --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsFeatureToggles.kt @@ -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") +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt new file mode 100644 index 0000000000..316720a3b6 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt @@ -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, + 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 + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ComponentModule.kt new file mode 100644 index 0000000000..f88b0b5ad0 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ComponentModule.kt @@ -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 +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/FeatureModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/FeatureModule.kt new file mode 100644 index 0000000000..a2085dfe77 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/FeatureModule.kt @@ -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) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ModelModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ModelModule.kt new file mode 100644 index 0000000000..9ee24735da --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ModelModule.kt @@ -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 +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt new file mode 100644 index 0000000000..6cbf590bc5 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt @@ -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 +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt new file mode 100644 index 0000000000..7929779ddb --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt @@ -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 \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItem.kt similarity index 96% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsListItem.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItem.kt index 7ceb15ca05..ff348ef270 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsListItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItem.kt @@ -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, ), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItemPlaceholder.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItemPlaceholder.kt new file mode 100644 index 0000000000..251844c2d6 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItemPlaceholder.kt @@ -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() + } + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/ListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/ListItem.kt new file mode 100644 index 0000000000..70f3921dac --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/ListItem.kt @@ -0,0 +1,3 @@ +package com.tangem.features.markets.ui.entity + +sealed class ListItem \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/models/MarketsListItemModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListItemUM.kt similarity index 91% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/models/MarketsListItemModel.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListItemUM.kt index 4c8d21f229..a9f12c9a19 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/models/MarketsListItemModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListItemUM.kt @@ -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, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt new file mode 100644 index 0000000000..b431e9e54c --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt @@ -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, + ) : ListUM() + + data object Loading : ListUM() + data object SearchNothingFound : ListUM() +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/preview/MarketChartListItemPreviewDataProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/preview/MarketChartListItemPreviewDataProvider.kt index 6121a35222..782d4bfbc8 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/preview/MarketChartListItemPreviewDataProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/preview/MarketChartListItemPreviewDataProvider.kt @@ -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( +internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider( 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( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 2af78f3dfd..9dad38b55e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -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, diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index bbfc624efd..c96d1c74b4 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -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) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index 7c3629f145..fa0bdbd72d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -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, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 815f0493b4..c90cea3084 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -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, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index d213d227f8..0666e1191a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -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() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 0de613caa1..41698402f7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -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, + marketsListComponent: MarketsListComponent?, + alertConfig: WalletAlertState?, onAutoScrollReset: () -> Unit, ) { var selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) } @@ -195,12 +216,39 @@ private fun WalletContent( ) } - BaseScaffold( - state = state, - selectedWallet = selectedWallet, - snackbarHostState = snackbarHostState, - ) { - scaffoldContent() + 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, + snackbarHostState = snackbarHostState, + ) { + scaffoldContent() + } } } @@ -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, + 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, ) } } diff --git a/tangem-android-tools b/tangem-android-tools index d8e6da9ab3..0ed07b85e6 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit d8e6da9ab3a0442657822352cb6274237d88e77d +Subproject commit 0ed07b85e64805707b2ef6a92f42bc2c4e8b7753