Updated on 2026-08-14
This commit is contained in:
parent
31f513725f
commit
d2d7dd6e71
17 changed files with 900 additions and 1 deletions
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<WalletState>,
|
||||
val onWalletChange: (Int) -> Unit,
|
||||
val event: StateEvent<WalletEvent>,
|
||||
val isHidingMode: Boolean,
|
||||
)
|
||||
|
||||
internal sealed class WalletState {
|
||||
|
||||
abstract val pullToRefreshConfig: WalletPullToRefreshConfig
|
||||
abstract val walletCardState: WalletCardState
|
||||
abstract val warnings: ImmutableList<WalletNotification>
|
||||
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<WalletNotification>,
|
||||
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<WalletNotification> = 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<WalletManageButton>
|
||||
abstract val marketPriceBlockState: MarketPriceBlockState?
|
||||
abstract val txHistoryState: TxHistoryState
|
||||
|
||||
data class Content(
|
||||
override val pullToRefreshConfig: WalletPullToRefreshConfig,
|
||||
override val walletCardState: WalletCardState,
|
||||
override val warnings: ImmutableList<WalletNotification>,
|
||||
override val bottomSheetConfig: TangemBottomSheetConfig?,
|
||||
override val buttons: PersistentList<WalletManageButton>,
|
||||
override val marketPriceBlockState: MarketPriceBlockState,
|
||||
override val txHistoryState: TxHistoryState,
|
||||
) : SingleCurrency()
|
||||
|
||||
data class Locked(
|
||||
override val walletCardState: WalletCardState,
|
||||
override val buttons: PersistentList<WalletManageButton>,
|
||||
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<WalletNotification> = 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<TokensListItemState>
|
||||
abstract val organizeTokensButtonConfig: OrganizeTokensButtonConfig?
|
||||
|
||||
object Loading : ContentState() {
|
||||
override val items = persistentListOf<TokensListItemState>()
|
||||
override val organizeTokensButtonConfig = null
|
||||
}
|
||||
|
||||
data class Content(
|
||||
override val items: ImmutableList<TokensListItemState>,
|
||||
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)
|
||||
|
|
@ -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<WalletScreenState> get() = mutableUiState
|
||||
val value: WalletScreenState get() = uiState.value
|
||||
|
||||
private val mutableUiState: MutableStateFlow<WalletScreenState> = 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -66,6 +66,7 @@ internal fun WalletEventEffect(
|
|||
}
|
||||
.addOnFailureListener(Timber::e)
|
||||
}
|
||||
is WalletEvent.DemonstrateWalletsScrollPreview -> Unit
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<WalletEvent>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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<WalletAlertState?>(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<Boolean>,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Boolean>,
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<WalletCardState>,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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<TxHistoryState.TxHistoryItemState>?,
|
||||
isBalanceHidden: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (state) {
|
||||
is WalletStateV2.MultiCurrency -> {
|
||||
tokensListItemsV2(state.tokensListState, modifier, isBalanceHidden)
|
||||
}
|
||||
is WalletStateV2.SingleCurrency -> {
|
||||
txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
|
|||
items(
|
||||
items = configs,
|
||||
key = { it::class.java },
|
||||
contentType = { it.config::class.java },
|
||||
contentType = { it::class.java },
|
||||
itemContent = {
|
||||
Notification(
|
||||
config = it.config,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState as WalletTokensListStateV2
|
||||
import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState.TokensListItemState as TokensListItemStateV2
|
||||
|
||||
private const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST"
|
||||
|
||||
|
|
@ -45,6 +47,23 @@ internal fun LazyListScope.tokensListItems(
|
|||
}
|
||||
}
|
||||
|
||||
internal fun LazyListScope.tokensListItemsV2(
|
||||
state: WalletTokensListStateV2,
|
||||
modifier: Modifier = Modifier,
|
||||
isBalanceHidden: Boolean,
|
||||
) {
|
||||
when (state) {
|
||||
is WalletTokensListStateV2.ContentState -> {
|
||||
contentItemsV2(
|
||||
items = state.items,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
WalletTokensListStateV2.Empty -> nonContentItem(modifier = modifier)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.contentItems(
|
||||
items: ImmutableList<WalletTokensListState.TokensListItemState>,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -67,6 +86,28 @@ private fun LazyListScope.contentItems(
|
|||
)
|
||||
}
|
||||
|
||||
private fun LazyListScope.contentItemsV2(
|
||||
items: ImmutableList<TokensListItemStateV2>,
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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<ReviewInfo>,
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<LazyListItemInfo>> {
|
||||
|
||||
private val LazyListItemInfo.halfItemSize
|
||||
get() = size.div(other = 2)
|
||||
|
||||
private var currentIndex = selectedWalletIndex
|
||||
|
||||
override suspend fun emit(value: List<LazyListItemInfo>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue