diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt index 9c619d9a9c..a269d87651 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt @@ -17,4 +17,16 @@ internal sealed class WalletEvent { data class CopyAddress(val address: String, val toast: TextReference) : WalletEvent() data class RateApp(val onDismissClick: () -> Unit) : WalletEvent() + + data class DemonstrateWalletsScrollPreview(val direction: Direction) : WalletEvent() { + + enum class Direction { + + /** 1 -> 2 */ + LEFT, + + /** 1 <- 2 */ + RIGHT, + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletScreenState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletScreenState.kt new file mode 100644 index 0000000000..740d54798c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletScreenState.kt @@ -0,0 +1,187 @@ +package com.tangem.feature.wallet.presentation.wallet.state2 + +import androidx.paging.PagingData +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state.components.* +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import javax.annotation.concurrent.Immutable + +const val NOT_INITIALIZED_WALLET_INDEX = -1 + +internal data class WalletScreenState( + val onBackClick: () -> Unit, + val topBarConfig: WalletTopBarConfig, + val selectedWalletIndex: Int, + val wallets: ImmutableList, + val onWalletChange: (Int) -> Unit, + val event: StateEvent, + val isHidingMode: Boolean, +) + +internal sealed class WalletState { + + abstract val pullToRefreshConfig: WalletPullToRefreshConfig + abstract val walletCardState: WalletCardState + abstract val warnings: ImmutableList + abstract val bottomSheetConfig: TangemBottomSheetConfig? + + sealed class MultiCurrency : WalletState() { + + abstract val tokensListState: WalletTokensListState + abstract val manageTokensButtonConfig: ManageTokensButtonConfig? + + data class Content( + override val pullToRefreshConfig: WalletPullToRefreshConfig, + override val walletCardState: WalletCardState, + override val warnings: ImmutableList, + override val bottomSheetConfig: TangemBottomSheetConfig?, + override val tokensListState: WalletTokensListState, + override val manageTokensButtonConfig: ManageTokensButtonConfig?, + ) : MultiCurrency() + + data class Locked( + override val walletCardState: WalletCardState, + val onUnlockNotificationClick: () -> Unit, + val isBottomSheetShow: Boolean = false, + val onBottomSheetDismiss: () -> Unit = {}, + val onUnlockClick: () -> Unit, + val onScanClick: () -> Unit, + ) : MultiCurrency() { + + override val pullToRefreshConfig: WalletPullToRefreshConfig + get() = WalletPullToRefreshConfig(isRefreshing = false, onRefresh = {}) + + override val warnings: ImmutableList = persistentListOf( + WalletNotification.UnlockWallets(onUnlockNotificationClick), + ) + + override val bottomSheetConfig = TangemBottomSheetConfig( + isShow = isBottomSheetShow, + onDismissRequest = onBottomSheetDismiss, + content = WalletBottomSheetConfig.UnlockWallets( + onUnlockClick = onUnlockClick, + onScanClick = onScanClick, + ), + ) + + override val tokensListState = WalletTokensListState.ContentState.Locked + override val manageTokensButtonConfig = null + } + } + + sealed class SingleCurrency : WalletState() { + + abstract val buttons: PersistentList + abstract val marketPriceBlockState: MarketPriceBlockState? + abstract val txHistoryState: TxHistoryState + + data class Content( + override val pullToRefreshConfig: WalletPullToRefreshConfig, + override val walletCardState: WalletCardState, + override val warnings: ImmutableList, + override val bottomSheetConfig: TangemBottomSheetConfig?, + override val buttons: PersistentList, + override val marketPriceBlockState: MarketPriceBlockState, + override val txHistoryState: TxHistoryState, + ) : SingleCurrency() + + data class Locked( + override val walletCardState: WalletCardState, + override val buttons: PersistentList, + val onUnlockNotificationClick: () -> Unit, + val isBottomSheetShow: Boolean = false, + val onBottomSheetDismiss: () -> Unit = {}, + val onUnlockClick: () -> Unit, + val onScanClick: () -> Unit, + val onExploreClick: () -> Unit, + ) : SingleCurrency() { + + override val pullToRefreshConfig: WalletPullToRefreshConfig + get() = WalletPullToRefreshConfig(isRefreshing = false, onRefresh = {}) + + override val warnings: ImmutableList = persistentListOf( + WalletNotification.UnlockWallets(onUnlockNotificationClick), + ) + + override val bottomSheetConfig = TangemBottomSheetConfig( + isShow = isBottomSheetShow, + onDismissRequest = onBottomSheetDismiss, + content = WalletBottomSheetConfig.UnlockWallets( + onUnlockClick = onUnlockClick, + onScanClick = onScanClick, + ), + ) + + override val marketPriceBlockState: MarketPriceBlockState? = null + + override val txHistoryState: TxHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = PagingData.from( + data = listOf( + TxHistoryState.TxHistoryItemState.Title(onExploreClick = onExploreClick), + TxHistoryState.TxHistoryItemState.Transaction( + state = TransactionState.Locked(txHash = "LOCKED_TX_HASH"), + ), + ), + ), + ), + ) + } + } +} + +internal sealed class WalletTokensListState { + + object Empty : WalletTokensListState() + + sealed class ContentState : WalletTokensListState() { + + abstract val items: ImmutableList + abstract val organizeTokensButtonConfig: OrganizeTokensButtonConfig? + + object Loading : ContentState() { + override val items = persistentListOf() + override val organizeTokensButtonConfig = null + } + + data class Content( + override val items: ImmutableList, + override val organizeTokensButtonConfig: OrganizeTokensButtonConfig?, + ) : ContentState() + + object Locked : ContentState() { + override val items = persistentListOf( + TokensListItemState.NetworkGroupTitle(id = 42, name = TextReference.Res(id = R.string.main_tokens)), + TokensListItemState.Token(state = TokenItemState.Locked(id = "Locked#1")), + ) + override val organizeTokensButtonConfig = null + } + } + + data class OrganizeTokensButtonConfig(val isEnabled: Boolean, val onClick: () -> Unit) + + @Immutable + sealed class TokensListItemState { + + abstract val id: Any + + data class NetworkGroupTitle(override val id: Int, val name: TextReference) : TokensListItemState() + + data class Token(val state: TokenItemState) : TokensListItemState() { + override val id: String = state.id + } + } +} + +internal data class ManageTokensButtonConfig(val onClick: () -> Unit) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletStateHolderV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletStateHolderV2.kt new file mode 100644 index 0000000000..c956da9c5d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletStateHolderV2.kt @@ -0,0 +1,54 @@ +package com.tangem.feature.wallet.presentation.wallet.state2 + +import com.tangem.core.ui.event.consumedEvent +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.WalletScreenStateTransformer +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Wallet state holder + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class WalletStateHolderV2 @Inject constructor() { + + val uiState: StateFlow get() = mutableUiState + val value: WalletScreenState get() = uiState.value + + private val mutableUiState: MutableStateFlow = MutableStateFlow(value = getInitialState()) + + fun update(function: (WalletScreenState) -> WalletScreenState) { + mutableUiState.update(function = function) + } + + fun update(transformer: WalletScreenStateTransformer) { + mutableUiState.update(function = transformer::transform) + } + + fun getSelectedWallet(): WalletState { + return with(value) { wallets[selectedWalletIndex] } + } + + fun getSelectedWalletId(): UserWalletId { + return with(value) { wallets[selectedWalletIndex].walletCardState.id } + } + + private fun getInitialState(): WalletScreenState { + return WalletScreenState( + onBackClick = {}, + topBarConfig = WalletTopBarConfig(onDetailsClick = {}), + selectedWalletIndex = NOT_INITIALIZED_WALLET_INDEX, + wallets = persistentListOf(), + onWalletChange = {}, + event = consumedEvent(), + isHidingMode = false, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletScreenStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletScreenStateTransformer.kt new file mode 100644 index 0000000000..05cbc33f27 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletScreenStateTransformer.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState + +internal interface WalletScreenStateTransformer { + + fun transform(prevState: WalletScreenState): WalletScreenState +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt index a6a26d1828..dbb5f28cfa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt @@ -66,6 +66,7 @@ internal fun WalletEventEffect( } .addOnFailureListener(Timber::e) } + is WalletEvent.DemonstrateWalletsScrollPreview -> Unit } }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffectV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffectV2.kt new file mode 100644 index 0000000000..73d54f4ddc --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffectV2.kt @@ -0,0 +1,62 @@ +package com.tangem.feature.wallet.presentation.wallet.ui + +import android.widget.Toast +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.AnnotatedString +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.ui.utils.ReviewManagerRequester +import com.tangem.feature.wallet.presentation.wallet.ui.utils.animateScrollByIndex +import com.tangem.feature.wallet.presentation.wallet.ui.utils.demonstrateScrolling + +@Suppress("LongParameterList") +@Composable +internal fun WalletEventEffectV2( + walletsListState: LazyListState, + snackbarHostState: SnackbarHostState, + event: StateEvent, + selectedWalletIndex: Int, + onAutoScrollSet: () -> Unit, + onAlertConfigSet: (WalletAlertState) -> Unit, +) { + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + val resources = LocalContext.current.resources + val clipboardManager = LocalClipboardManager.current + EventEffect( + event = event, + onTrigger = { value -> + when (value) { + is WalletEvent.ChangeWallet -> { + onAutoScrollSet() + walletsListState.animateScrollByIndex(prevIndex = selectedWalletIndex, newIndex = value.index) + } + is WalletEvent.ShowError -> { + snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) + } + is WalletEvent.ShowToast -> { + Toast.makeText(context, value.text.resolveReference(resources), Toast.LENGTH_SHORT).show() + } + is WalletEvent.CopyAddress -> { + clipboardManager.setText(AnnotatedString(value.address)) + Toast.makeText(context, value.toast.resolveReference(resources), Toast.LENGTH_SHORT).show() + } + is WalletEvent.ShowAlert -> onAlertConfigSet(value.state) + is WalletEvent.RateApp -> { + ReviewManagerRequester.request(context = context, onDismissClick = value.onDismissClick) + } + is WalletEvent.DemonstrateWalletsScrollPreview -> { + walletsListState.demonstrateScrolling(coroutineScope = coroutineScope, direction = value.direction) + } + } + }, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreenV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreenV2.kt new file mode 100644 index 0000000000..fc946a27f4 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreenV2.kt @@ -0,0 +1,243 @@ +package com.tangem.feature.wallet.presentation.wallet.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +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.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.paging.compose.collectAsLazyPagingItems +import com.tangem.core.ui.components.PrimaryButton +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.transactions.state.TxHistoryState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state2.NOT_INITIALIZED_WALLET_INDEX +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet +import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList +import com.tangem.feature.wallet.presentation.wallet.ui.components.common.* +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton +import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.controlButtons +import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock +import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun WalletScreenV2(state: WalletScreenState) { + BackHandler(onBack = state.onBackClick) + + // It means that screen is still initializing + if (state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX) return + + val walletsListState = rememberLazyListState(initialFirstVisibleItemIndex = state.selectedWalletIndex) + val snackbarHostState = remember(::SnackbarHostState) + val isAutoScroll = remember { mutableStateOf(value = false) } + + WalletContent( + state = state, + walletsListState = walletsListState, + snackbarHostState = snackbarHostState, + isAutoScroll = isAutoScroll, + onAutoScrollReset = { isAutoScroll.value = false }, + ) + + var alertConfig by remember { mutableStateOf(value = null) } + + alertConfig?.let { + WalletAlert(state = it, onDismiss = { alertConfig = null }) + } + + WalletEventEffectV2( + event = state.event, + selectedWalletIndex = state.selectedWalletIndex, + walletsListState = walletsListState, + snackbarHostState = snackbarHostState, + onAlertConfigSet = { alertConfig = it }, + onAutoScrollSet = { isAutoScroll.value = true }, + ) +} + +@Suppress("LongMethod") +@Composable +private fun WalletContent( + state: WalletScreenState, + walletsListState: LazyListState, + snackbarHostState: SnackbarHostState, + isAutoScroll: State, + onAutoScrollReset: () -> Unit, +) { + var selectedWalletIndex by remember { mutableIntStateOf(state.selectedWalletIndex) } + val selectedWallet = state.wallets[selectedWalletIndex] + + BaseScaffold(state = state, selectedWallet = selectedWallet, snackbarHostState = snackbarHostState) { + val movableItemModifier = Modifier.changeWalletAnimator(walletsListState) + + val lazyTxHistoryItems = (selectedWallet as? WalletState.SingleCurrency)?.let { walletState -> + (walletState.txHistoryState as? TxHistoryState.Content)?.contentItems?.collectAsLazyPagingItems() + } + + val txHistoryItems by remember(selectedWallet.walletCardState.id, lazyTxHistoryItems?.itemCount) { + mutableStateOf(value = lazyTxHistoryItems) + } + + val betweenItemsPadding = TangemTheme.dimens.spacing14 + val horizontalPadding = TangemTheme.dimens.spacing16 + val itemModifier = movableItemModifier + .padding(top = betweenItemsPadding) + .padding(horizontal = horizontalPadding) + + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = TangemTheme.dimens.spacing8, + bottom = TangemTheme.dimens.spacing92, + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + item( + key = state.wallets.map { it.walletCardState.id }, + contentType = state.wallets.map { it.walletCardState.id }, + ) { + WalletsList( + lazyListState = walletsListState, + wallets = state.wallets.map(WalletState::walletCardState).toImmutableList(), + isBalanceHidden = state.isHidingMode, + ) + } + + (selectedWallet as? WalletState.SingleCurrency)?.let { + controlButtons( + configs = it.buttons, + selectedWalletIndex = selectedWalletIndex, + modifier = movableItemModifier.padding(top = betweenItemsPadding), + ) + } + + notifications(configs = selectedWallet.warnings, modifier = itemModifier) + + (selectedWallet as? WalletState.SingleCurrency)?.let { walletState -> + walletState.marketPriceBlockState?.let { marketPriceBlockState -> + marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier) + } + } + + contentItemsV2( + state = selectedWallet, + txHistoryItems = txHistoryItems, + isBalanceHidden = state.isHidingMode, + modifier = movableItemModifier, + ) + + organizeTokens(state = selectedWallet, itemModifier = itemModifier) + } + + val bottomSheetConfig = selectedWallet.bottomSheetConfig + if (bottomSheetConfig != null) { + when (bottomSheetConfig.content) { + is WalletBottomSheetConfig -> WalletBottomSheet(config = bottomSheetConfig) + is TokenReceiveBottomSheetConfig -> TokenReceiveBottomSheet(config = bottomSheetConfig) + is ActionsBottomSheetConfig -> TokenActionsBottomSheet(config = bottomSheetConfig) + is ChooseAddressBottomSheetConfig -> ChooseAddressBottomSheet(config = bottomSheetConfig) + } + } + + WalletsListEffectsV2( + lazyListState = walletsListState, + selectedWalletIndex = selectedWalletIndex, + onWalletChange = state.onWalletChange, + onSelectedWalletIndexSet = { selectedWalletIndex = it }, + isAutoScroll = isAutoScroll, + onAutoScrollReset = onAutoScrollReset, + ) + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun BaseScaffold( + state: WalletScreenState, + selectedWallet: WalletState, + snackbarHostState: SnackbarHostState, + content: @Composable () -> Unit, +) { + Scaffold( + topBar = { WalletTopBar(config = state.topBarConfig) }, + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + floatingActionButton = { + val manageTokensButtonConfig by remember(state.selectedWalletIndex) { + mutableStateOf( + (state.wallets[state.selectedWalletIndex] as? WalletState.MultiCurrency)?.manageTokensButtonConfig, + ) + } + + manageTokensButtonConfig?.let { ManageTokensButton(onClick = it.onClick) } + }, + floatingActionButtonPosition = FabPosition.Center, + containerColor = TangemTheme.colors.background.secondary, + content = { + val pullRefreshState = rememberPullRefreshState( + refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, + onRefresh = selectedWallet.pullToRefreshConfig.onRefresh, + ) + + Box( + modifier = Modifier + .pullRefresh(pullRefreshState) + .padding(it), + ) { + content() + + WalletPullToRefreshIndicator( + isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + ) + } + }, + ) +} + +@Composable +private fun ManageTokensButton(onClick: () -> Unit) { + PrimaryButton( + text = stringResource(id = R.string.main_manage_tokens), + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + ) +} + +internal fun LazyListScope.organizeTokens(state: WalletState, itemModifier: Modifier) { + (state as? WalletState.MultiCurrency)?.let { + (state.tokensListState as? WalletTokensListState.ContentState)?.let { + it.organizeTokensButtonConfig?.let { config -> + organizeTokensButton( + modifier = itemModifier, + isEnabled = config.isEnabled, + onClick = config.onClick, + ) + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffectsV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffectsV2.kt new file mode 100644 index 0000000000..d7dc0bb129 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffectsV2.kt @@ -0,0 +1,45 @@ +package com.tangem.feature.wallet.presentation.wallet.ui + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.snapshotFlow +import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollectorV2 +import com.tangem.feature.wallet.presentation.wallet.ui.utils.WalletsListInteractionsCollector + +@Suppress("LongParameterList") +@Composable +internal fun WalletsListEffectsV2( + lazyListState: LazyListState, + selectedWalletIndex: Int, + onWalletChange: (Int) -> Unit, + onSelectedWalletIndexSet: (Int) -> Unit, + isAutoScroll: State, + onAutoScrollReset: () -> Unit, +) { + LaunchedEffect(key1 = lazyListState, key2 = onWalletChange) { + snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo } + .collect( + collector = ScrollOffsetCollectorV2( + selectedWalletIndex = selectedWalletIndex, + lazyListState = lazyListState, + onWalletChange = { newIndex -> + // Auto scroll must not change wallet + if (isAutoScroll.value) { + onSelectedWalletIndexSet(newIndex) + } else { + onSelectedWalletIndexSet(newIndex) + onWalletChange(newIndex) + } + }, + ), + ) + } + + LaunchedEffect(Unit) { + lazyListState.interactionSource.interactions.collect( + collector = WalletsListInteractionsCollector(onDragStart = onAutoScrollReset), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index 7caf718821..ff70bf8486 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -23,8 +23,10 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletCard +import kotlinx.collections.immutable.ImmutableList private const val SHORT_SNAP_ELEMENT_COUNT = 50 @@ -66,6 +68,40 @@ internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState } } +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun WalletsList( + lazyListState: LazyListState, + wallets: ImmutableList, + isBalanceHidden: Boolean, +) { + val horizontalCardPadding = TangemTheme.dimens.spacing16 + val screenWidth = LocalConfiguration.current.screenWidthDp.dp + val itemWidth by remember(screenWidth) { derivedStateOf { screenWidth - horizontalCardPadding * 2 } } + + LazyRow( + modifier = Modifier.background(color = TangemTheme.colors.background.secondary), + state = lazyListState, + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + flingBehavior = rememberWalletsFlingBehaviour(lazyListState = lazyListState, itemWidth = itemWidth), + ) { + items( + items = wallets, + key = { it.id.stringValue }, + contentType = { it.id.stringValue }, + ) { state -> + WalletCard( + state = state, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .animateItemPlacement() + .width(itemWidth), + ) + } + } +} + /** * Custom implementation of fling behaviour that overrides 'shortSnapVelocityThreshold'. * Every user's drag action will similar to a short snap diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt index 13ff628917..8ad776a987 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt @@ -9,6 +9,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencySt import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.tokensListItems +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.tokensListItemsV2 +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState as WalletStateV2 /** * Wallet content @@ -29,4 +31,20 @@ internal fun LazyListScope.contentItems( is WalletMultiCurrencyState -> tokensListItems(state.tokensListState, modifier, isBalanceHidden) is WalletSingleCurrencyState -> txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier) } +} + +internal fun LazyListScope.contentItemsV2( + state: WalletStateV2, + txHistoryItems: LazyPagingItems?, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + when (state) { + is WalletStateV2.MultiCurrency -> { + tokensListItemsV2(state.tokensListState, modifier, isBalanceHidden) + } + is WalletStateV2.SingleCurrency -> { + txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier) + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index e60ec4b043..9e0f9f5123 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -22,7 +22,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList { + contentItemsV2( + items = state.items, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + ) + } + WalletTokensListStateV2.Empty -> nonContentItem(modifier = modifier) + } +} + private fun LazyListScope.contentItems( items: ImmutableList, modifier: Modifier = Modifier, @@ -67,6 +86,28 @@ private fun LazyListScope.contentItems( ) } +private fun LazyListScope.contentItemsV2( + items: ImmutableList, + modifier: Modifier = Modifier, + isBalanceHidden: Boolean, +) { + itemsIndexed( + items = items, + key = { _, item -> item.id }, + contentType = { _, item -> item::class.java }, + itemContent = { index, item -> + MultiCurrencyContentItem( + state = item, + isBalanceHidden = isBalanceHidden, + modifier = modifier.roundedShapeItemDecoration( + currentIndex = index, + lastIndex = items.lastIndex, + ), + ) + }, + ) +} + @OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) { item( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt index 9678a22d13..48f04fb8ff 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.feature.wallet.presentation.common.component.NetworkGroupItem import com.tangem.feature.wallet.presentation.common.component.TokenItem import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState.TokensListItemState /** * Multi-currency content item @@ -29,4 +30,20 @@ internal fun MultiCurrencyContentItem( TokenItem(state = state.state, isBalanceHidden = isBalanceHidden, modifier = modifier) } } +} + +@Composable +internal fun MultiCurrencyContentItem( + state: TokensListItemState, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + when (state) { + is TokensListItemState.NetworkGroupTitle -> { + NetworkGroupItem(networkName = state.name.resolveReference(), modifier = modifier) + } + is TokensListItemState.Token -> { + TokenItem(state = state.state, isBalanceHidden = isBalanceHidden, modifier = modifier) + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt new file mode 100644 index 0000000000..a9ccd192d4 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt @@ -0,0 +1,24 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.utils + +import androidx.compose.animation.core.tween +import androidx.compose.foundation.gestures.animateScrollBy +import androidx.compose.foundation.lazy.LazyListLayoutInfo +import androidx.compose.foundation.lazy.LazyListState + +/** + * Animate scroll [LazyListState]. + * + * [LazyListState] method for scroll with animation by index isn't supported custom animation. + * This extension method calculate offset between [prevIndex] and [newIndex], + * and scroll by it with default animation. + */ +internal suspend fun LazyListState.animateScrollByIndex(prevIndex: Int, newIndex: Int) { + animateScrollBy( + value = calculateOffset(layoutInfo, prevIndex, newIndex), + animationSpec = tween(durationMillis = 1000), + ) +} + +private fun calculateOffset(layoutInfo: LazyListLayoutInfo, prevIndex: Int, newIndex: Int): Float { + return layoutInfo.viewportSize.width.times(other = newIndex - prevIndex).toFloat() +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt new file mode 100644 index 0000000000..a52e61dd42 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt @@ -0,0 +1,56 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.utils + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import com.google.android.play.core.review.ReviewInfo +import com.google.android.play.core.review.ReviewManager +import com.google.android.play.core.review.ReviewManagerFactory +import com.google.android.play.core.tasks.Task +import timber.log.Timber + +internal object ReviewManagerRequester { + + fun request(context: Context, onDismissClick: () -> Unit) { + val reviewManager = ReviewManagerFactory.create(context) + val requestTask = reviewManager.requestReviewFlow() + + requestTask + .addOnCompleteListener { + handleOnCompleteRequestTask( + reviewManager = reviewManager, + activity = context.findActivity(), + task = it, + onDismissClick = onDismissClick, + ) + } + .addOnFailureListener(Timber::e) + } + + private fun handleOnCompleteRequestTask( + reviewManager: ReviewManager, + activity: Activity, + task: Task, + onDismissClick: () -> Unit, + ) { + if (task.isSuccessful) { + val reviewFlow = reviewManager.launchReviewFlow(activity, task.result) + reviewFlow + .addOnCompleteListener { resultReviewTask -> + if (!resultReviewTask.isSuccessful) onDismissClick() + } + .addOnFailureListener(Timber::e) + } else { + Timber.e(task.exception) + } + } + + private fun Context.findActivity(): Activity { + var context = this + while (context is ContextWrapper) { + if (context is Activity) return context + context = context.baseContext + } + error("Permissions should be called in the context of an Activity") + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollectorV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollectorV2.kt new file mode 100644 index 0000000000..43a4af298b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollectorV2.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.utils + +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListState +import kotlinx.coroutines.flow.FlowCollector +import kotlin.math.abs + +/** + * Flow collector for scroll items tracking. + * If first visible item offset is greater than half item size, then change selected wallet index. + * If last visible item offset is greater than half item size, then change selected wallet index. + * + * @param selectedWalletIndex selected wallet index + * @property lazyListState lazy list state + * @property onWalletChange callback that will be invoked on wallet change + * +[REDACTED_AUTHOR] + */ +internal class ScrollOffsetCollectorV2( + selectedWalletIndex: Int, + private val lazyListState: LazyListState, + private val onWalletChange: (Int) -> Unit, +) : FlowCollector> { + + private val LazyListItemInfo.halfItemSize + get() = size.div(other = 2) + + private var currentIndex = selectedWalletIndex + + override suspend fun emit(value: List) { + if (!lazyListState.isScrollInProgress || value.size <= 1) return + + val firstItem = value.firstOrNull() ?: return + val lastItem = value.lastOrNull() ?: return + + if (abs(firstItem.offset) > firstItem.halfItemSize) { + selectIndex(newIndex = firstItem.index + 1) + } else if (abs(lastItem.offset) > lastItem.halfItemSize) { + selectIndex(newIndex = lastItem.index - 1) + } + } + + private fun selectIndex(newIndex: Int) { + if (currentIndex != newIndex) { + currentIndex = newIndex + onWalletChange(newIndex) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletsScrollPreviewExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletsScrollPreviewExt.kt new file mode 100644 index 0000000000..3dbb6ccb11 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletsScrollPreviewExt.kt @@ -0,0 +1,46 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.utils + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.gestures.animateScrollBy +import androidx.compose.foundation.lazy.LazyListState +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent.DemonstrateWalletsScrollPreview +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +private const val VISIBLE_PART_OF_WALLET_CARD = 0.2f + +internal fun LazyListState.demonstrateScrolling( + coroutineScope: CoroutineScope, + direction: DemonstrateWalletsScrollPreview.Direction, +) { + coroutineScope.launch { + animateScrollBy( + value = calculateOffset(direction = direction, isReverse = false), + animationSpec = spring(stiffness = Spring.StiffnessMediumLow), + ) + } + .invokeOnCompletion { + coroutineScope.launch { + animateScrollBy( + value = calculateOffset(direction = direction, isReverse = true), + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow, + ), + ) + } + } +} + +private fun LazyListState.calculateOffset( + direction: DemonstrateWalletsScrollPreview.Direction, + isReverse: Boolean, +): Float { + val sign = when (direction) { + DemonstrateWalletsScrollPreview.Direction.LEFT -> 1 + DemonstrateWalletsScrollPreview.Direction.RIGHT -> -1 + }.times(other = if (isReverse) -1 else 1) + + return layoutInfo.viewportSize.width.toFloat() * VISIBLE_PART_OF_WALLET_CARD * sign +} \ No newline at end of file