From 51540c732a5a9e40c158160ad5d047d504e5bef1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Nov 2023 15:44:02 +0400 Subject: [PATCH 01/20] Updated on 2026-08-14 --- .../components/SettingsSwitchItem.kt | 1 + .../com/tangem/core/ui/components/Cards.kt | 22 +- .../tangem/core/ui/components/TangemSwitch.kt | 2 +- .../com/tangem/core/ui/components/Warnings.kt | 41 +++- .../core/ui/decorations/RoundedDecorations.kt | 32 ++- .../common/state/ChooseWalletState.kt | 31 +++ .../common/state/NetworkItemState.kt | 90 ++++++++ .../presentation/common/state/WalletState.kt | 8 + .../ChooseWalletStatePreviewData.kt | 27 +++ .../common/ui/ChooseWalletScreen.kt | 151 +++++++++++++ .../common/ui/components/NetworkItem.kt | 157 ++++++++++++++ .../ui/components/SimpleSelectionBlock.kt | 70 ++++++ .../managetokens/state/ChooseNetworkState.kt | 11 + .../state/DerivationNotificationState.kt | 7 +- .../managetokens/state/TokenItemState.kt | 10 +- .../ChooseNetworkStatePreviewData.kt | 60 +++++ .../DerivationNotificationStatePreviewData.kt | 5 +- .../previewdata/TokenItemStatePreviewData.kt | 13 +- .../managetokens/ui/ChooseNetworkScreen.kt | 205 ++++++++++++++++++ .../ui/components/TokenRowItem.kt | 6 +- 20 files changed, 915 insertions(+), 34 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/WalletState.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/previewdata/ChooseWalletStatePreviewData.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ChooseNetworkState.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt index 74815a675f..b584136754 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt @@ -63,6 +63,7 @@ internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier checked = item.isChecked, enabled = item.isEnabled, onCheckedChange = item.onCheckedChange, + checkedColor = TangemTheme.colors.icon.accent, ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt index 847c27307f..91dd9a2506 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt @@ -13,6 +13,7 @@ import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -162,11 +163,12 @@ fun CardWithIcon( * >Figma component */ @Composable -fun IconWithTitleAndDescription( +internal fun IconWithTitleAndDescription( title: String, - description: String, + description: String?, icon: @Composable () -> Unit, additionalContent: @Composable () -> Unit = {}, + iconBackground: Color = TangemTheme.colors.background.secondary, ) { Row( modifier = Modifier @@ -182,7 +184,7 @@ fun IconWithTitleAndDescription( Box( modifier = Modifier .background( - color = TangemTheme.colors.background.secondary, + color = iconBackground, shape = CircleShape, ) .height(TangemTheme.dimens.size40) @@ -205,12 +207,14 @@ fun IconWithTitleAndDescription( color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.subtitle1, ) - SpacerH4() - Text( - text = description, - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - ) + if (description != null) { + SpacerH4() + Text( + text = description, + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + ) + } } additionalContent() diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt index c4d41ef91b..8e0081a633 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt @@ -23,7 +23,7 @@ import com.tangem.core.ui.res.TangemTheme @Composable fun TangemSwitch( onCheckedChange: (Boolean) -> Unit, - checkedColor: Color = TangemTheme.colors.icon.accent, + checkedColor: Color = TangemTheme.colors.control.checked, uncheckedColor: Color = TangemTheme.colors.icon.informative, size: Dp = 48.dp, checked: Boolean = false, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt index b6b858d2cd..d0d3315f20 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt @@ -9,6 +9,7 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R @@ -36,6 +37,28 @@ fun WarningCard(title: String, description: String, icon: @Composable (() -> Uni ) } +/** + * A card with a warning icon to the left and title without description shown to the right of it. + * + * @param title title of the warning in bold + * + * @see Figma component + */ +@Composable +fun WarningCardTitleOnly(title: String, icon: @Composable (() -> Unit)? = null) { + WarningCardMaterial3Style( + content = { + WarningBody( + title = title, + description = null, + icon = icon, + iconBackground = TangemTheme.colors.button.disabled, + ) + }, + ) +} + /** * [WarningCard], but clickable (with an 'greater then' icon to the left) * @@ -105,7 +128,8 @@ fun RefreshableWaringCard( @Composable private fun WarningBody( title: String, - description: String, + description: String?, + iconBackground: Color = TangemTheme.colors.background.secondary, icon: @Composable (() -> Unit)? = null, additionalContent: @Composable () -> Unit = {}, ) { @@ -119,6 +143,7 @@ private fun WarningBody( contentDescription = null, ) }, + iconBackground = iconBackground, ) } @@ -136,6 +161,20 @@ private fun WarningCardSurface(onClick: (() -> Unit)? = null, content: @Composab } } +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun WarningCardMaterial3Style(onClick: (() -> Unit)? = null, content: @Composable () -> Unit) { + Card( + shape = RoundedCornerShape(TangemTheme.dimens.radius16), + backgroundColor = TangemTheme.colors.button.disabled, + elevation = TangemTheme.dimens.elevation0, + onClick = onClick ?: {}, + enabled = onClick != null, + ) { + content() + } +} + // endregion elements // region Preview diff --git a/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt b/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt index 362f4d95e9..4a014cd824 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt @@ -7,18 +7,34 @@ import androidx.compose.ui.composed import androidx.compose.ui.draw.clip import com.tangem.core.ui.res.TangemTheme -fun Modifier.roundedShapeItemDecoration(currentIndex: Int, lastIndex: Int): Modifier = composed { - val modifierWithHorizontalPadding = this.padding(horizontal = TangemTheme.dimens.spacing16) +fun Modifier.roundedShapeItemDecoration( + currentIndex: Int, + lastIndex: Int, + addDefaultPadding: Boolean = true, +): Modifier = composed { + val modifier = if (addDefaultPadding) this.padding(horizontal = TangemTheme.dimens.spacing16) else this val isSingleItem = currentIndex == 0 && lastIndex == 0 when { isSingleItem -> { - modifierWithHorizontalPadding - .padding(top = TangemTheme.dimens.spacing14) + modifier + .then( + if (addDefaultPadding) { + Modifier.padding(top = TangemTheme.dimens.spacing14) + } else { + Modifier + }, + ) .clip(shape = TangemTheme.shapes.roundedCornersXMedium) } currentIndex == 0 -> { - modifierWithHorizontalPadding - .padding(top = TangemTheme.dimens.spacing14) + modifier + .then( + if (addDefaultPadding) { + Modifier.padding(top = TangemTheme.dimens.spacing14) + } else { + Modifier + }, + ) .clip( shape = RoundedCornerShape( topStart = TangemTheme.dimens.radius16, @@ -27,7 +43,7 @@ fun Modifier.roundedShapeItemDecoration(currentIndex: Int, lastIndex: Int): Modi ) } currentIndex == lastIndex -> { - modifierWithHorizontalPadding + modifier .clip( shape = RoundedCornerShape( bottomStart = TangemTheme.dimens.radius16, @@ -35,6 +51,6 @@ fun Modifier.roundedShapeItemDecoration(currentIndex: Int, lastIndex: Int): Modi ), ) } - else -> modifierWithHorizontalPadding + else -> modifier } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt new file mode 100644 index 0000000000..44a0529872 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt @@ -0,0 +1,31 @@ +package com.tangem.managetokens.presentation.common.state + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.managetokens.impl.R +import kotlinx.collections.immutable.ImmutableList + +internal sealed class ChooseWalletState { + data class Choose( + val wallets: ImmutableList, + val selectedWallet: WalletState?, + val onChooseWalletClick: () -> Unit, + val onCloseChoosingWalletClick: () -> Unit, + ) : ChooseWalletState() + + object NoSelection : ChooseWalletState() + + class Warning(val type: ChooseWalletWarning) : ChooseWalletState() { + val message: TextReference + get() = when (type) { + ChooseWalletWarning.SINGLE_CURRENCY -> + TextReference.Res(R.string.manage_tokens_wallet_support_only_one_network_title) + ChooseWalletWarning.WALLET_INCOMPATIBLE -> + TextReference.Res(R.string.manage_tokens_wallet_does_not_supported_blockchain) + } + } +} + +enum class ChooseWalletWarning { + SINGLE_CURRENCY, + WALLET_INCOMPATIBLE, +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt new file mode 100644 index 0000000000..a759836afc --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt @@ -0,0 +1,90 @@ +package com.tangem.managetokens.presentation.common.state + +import androidx.compose.runtime.MutableState +import com.tangem.blockchain.common.Blockchain +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.getGreyedOutIconRes +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState + +/** + * Network item state + * + * @property name network name + * @property protocolName network protocol name + * @property id network id + * @property blockchain blockchain + * @property iconRes network icon id from resources + */ +internal sealed interface NetworkItemState { + + val name: String + val protocolName: String + val id: String + val blockchain: Blockchain + + val iconRes: Int + get() = when (this) { + is Selectable -> this.iconResId + is Toggleable -> this.iconResId.value + } + + /** + * Network item state that can be added and deleted + * + * @property name network name + * @property protocolName network protocol name + * @property id network id + * @property blockchain blockchain + * @property iconResId network icon id from resources + * @property isMainNetwork flag that determines if the network is the main network for the token + * @property isAdded flag that determines if the user has saved the network + * @property address contract address + * @property decimals decimal count + * @property onToggleClick lambda be invoked when switch is been toggled + */ + @Suppress("LongParameterList") + class Toggleable( + override val name: String, + override val protocolName: String, + override val id: String, + override val blockchain: Blockchain, + val iconResId: MutableState, + val isMainNetwork: Boolean, + val isAdded: MutableState, + val address: String?, + val decimals: Int?, + val onToggleClick: (TokenItemState.Loaded, Toggleable) -> Unit, + ) : NetworkItemState { + + /** + * Change toggle state [isAdded]. + * + * It is a hack that helps us to change element of flow + */ + fun changeToggleState() { + val reverseState = !isAdded.value + isAdded.value = reverseState + iconResId.value = if (reverseState) getActiveIconRes(blockchain.id) else getGreyedOutIconRes(blockchain.id) + } + } + + /** + * Network item state that can be selected + * + * @property name network name + * @property protocolName network protocol name + * @property iconResId network icon id from resources + * @property id network id + * @property blockchain blockchain + * @property onNetworkClick lambda be invoked when network item is been clicked + * + */ + class Selectable( + override val name: String, + override val protocolName: String, + val iconResId: Int, + override val id: String, + override val blockchain: Blockchain, + val onNetworkClick: (NetworkItemState) -> Unit, + ) : NetworkItemState +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/WalletState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/WalletState.kt new file mode 100644 index 0000000000..6af48736a4 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/WalletState.kt @@ -0,0 +1,8 @@ +package com.tangem.managetokens.presentation.common.state + +internal data class WalletState( + val walletId: String, + val artworkUrl: String?, + val walletName: String, + val onSelected: (String) -> Unit, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/previewdata/ChooseWalletStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/previewdata/ChooseWalletStatePreviewData.kt new file mode 100644 index 0000000000..f231654b96 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/previewdata/ChooseWalletStatePreviewData.kt @@ -0,0 +1,27 @@ +package com.tangem.managetokens.presentation.common.state.previewdata + +import com.tangem.managetokens.presentation.common.state.ChooseWalletState +import com.tangem.managetokens.presentation.common.state.WalletState +import kotlinx.collections.immutable.persistentListOf + +internal object ChooseWalletStatePreviewData { + + val state: ChooseWalletState.Choose + get() = ChooseWalletState.Choose( + wallets = persistentListOf( + walletState, + walletState.copy(walletId = "2"), + ), + selectedWallet = walletState, + onChooseWalletClick = {}, + onCloseChoosingWalletClick = {}, + ) + + private val walletState: WalletState + get() = WalletState( + walletName = "My wallet", + walletId = "1", + artworkUrl = "", + onSelected = {}, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt new file mode 100644 index 0000000000..d0dab333d8 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt @@ -0,0 +1,151 @@ +package com.tangem.managetokens.presentation.common.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.common.state.ChooseWalletState +import com.tangem.managetokens.presentation.common.state.WalletState +import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData + +@Composable +internal fun ChooseWalletScreen(state: ChooseWalletState.Choose, modifier: Modifier = Modifier) { + LazyColumn( + modifier = modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing16), + ) { + item { + Box( + modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size44), + ) { + IconButton( + onClick = state.onCloseChoosingWalletClick, + modifier = Modifier.align(Alignment.CenterStart), + ) { + Icon( + painterResource(id = R.drawable.ic_back_24), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + ) + } + Text( + text = stringResource(id = R.string.manage_tokens_wallet_selector_title), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + textAlign = TextAlign.Center, + maxLines = 1, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Center), + ) + } + } + items( + count = state.wallets.count(), + key = { index -> state.wallets[index].walletId }, + ) { index -> + WalletItem( + wallet = state.wallets[index], + selectedWallet = state.selectedWallet, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = state.wallets.lastIndex, + addDefaultPadding = false, + ), + ) + } + item { + SpacerH(height = TangemTheme.dimens.spacing16) + } + } +} + +@Composable +private fun WalletItem(wallet: WalletState, selectedWallet: WalletState?, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .clickable { wallet.onSelected(wallet.walletId) } + .background(TangemTheme.colors.background.action) + .defaultMinSize(minHeight = TangemTheme.dimens.size72) + .padding(horizontal = TangemTheme.dimens.spacing16), + verticalAlignment = Alignment.CenterVertically, + ) { + SubcomposeAsyncImage( + modifier = Modifier.size(height = TangemTheme.dimens.size30, width = TangemTheme.dimens.size50), + + model = ImageRequest.Builder(context = LocalContext.current) + .data(wallet.artworkUrl) + .crossfade(enable = true) + .build(), + loading = { + Image( + painter = painterResource(R.drawable.card_placeholder_primary), + contentDescription = null, + ) + }, + error = { + Image( + painter = painterResource(R.drawable.card_placeholder_primary), + contentDescription = null, + ) + }, + contentDescription = null, + ) + SpacerW12() + Text( + text = wallet.walletName, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + SpacerWMax() + if (selectedWallet == wallet) { + Icon( + painter = painterResource(id = R.drawable.ic_check_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + } + } +} + +@Preview +@Composable +private fun Preview_ChooseWalletScreen_Light() { + TangemTheme(isDark = false) { + ChooseWalletScreen( + state = ChooseWalletStatePreviewData.state, + ) + } +} + +@Preview +@Composable +private fun Preview_ChooseWalletScreen_Dark() { + TangemTheme(isDark = false) { + ChooseWalletScreen( + state = ChooseWalletStatePreviewData.state, + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt new file mode 100644 index 0000000000..18de0a171a --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt @@ -0,0 +1,157 @@ +package com.tangem.managetokens.presentation.common.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.blockchain.common.Blockchain +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.common.state.NetworkItemState +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState +import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData + +@Composable +internal fun NetworkItem(state: NetworkItemState, tokenState: TokenItemState.Loaded?, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .background(TangemTheme.colors.background.action) + .defaultMinSize(minHeight = TangemTheme.dimens.size68) + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + NetworkIcon(model = state) + SpacerW(width = TangemTheme.dimens.spacing12) + Text( + text = state.name, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + ) + SpacerW(width = TangemTheme.dimens.spacing6) + Text( + text = state.protocolName, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + modifier = Modifier + .weight(1f), + ) + if (state is NetworkItemState.Toggleable) { + TangemSwitch( + onCheckedChange = { + state.onToggleClick(tokenState!!, state) + }, + checked = state.isAdded.value, + ) + } + } +} + +@Composable +internal fun NetworkIcon(model: NetworkItemState, modifier: Modifier = Modifier) { + Box(modifier = modifier.size(size = TangemTheme.dimens.size36)) { + val isAdded = when (model) { + is NetworkItemState.Selectable -> true + is NetworkItemState.Toggleable -> model.isAdded.value + } + + if (!isAdded) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size36) + .clip(CircleShape) + .background(TangemTheme.colors.control.unchecked), + ) + } + Icon( + painter = painterResource(id = model.iconRes), + contentDescription = null, + modifier = Modifier.size(size = TangemTheme.dimens.size36), + tint = if (isAdded) Color.Unspecified else TangemTheme.colors.text.tertiary, + ) + + if (model is NetworkItemState.Toggleable && model.isMainNetwork) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .size(TangemTheme.dimens.size10) + .clip(CircleShape) + .background(TangemTheme.colors.stroke.transparency), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size8) + .clip(CircleShape) + .background(TangemTheme.colors.icon.accent), + ) + } + } + } +} + +@Preview +@Composable +private fun Preview_NetworkItem_Light(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) { + TangemTheme(isDark = false) { + NetworkItem(state, tokenState = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded) + } +} + +@Preview +@Composable +private fun Preview_NetworkItem_Dark(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) { + TangemTheme(isDark = true) { + NetworkItem(state, tokenState = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded) + } +} + +private class NetworkItemStateProvider : CollectionPreviewParameterProvider( + collection = listOf( + NetworkItemState.Toggleable( + name = "Ethereum", + protocolName = "ETH", + iconResId = mutableStateOf(R.drawable.img_polygon_22), + isMainNetwork = true, + isAdded = mutableStateOf(true), + id = "", + address = "", + onToggleClick = { _, _ -> }, + blockchain = Blockchain.Ethereum, + decimals = 0, + ), + NetworkItemState.Toggleable( + name = "BNB SMART CHAIN", + protocolName = "BEP20", + iconResId = mutableStateOf(R.drawable.ic_bsc_16), + isMainNetwork = false, + isAdded = mutableStateOf(false), + id = "", + address = "", + onToggleClick = { _, _ -> }, + blockchain = Blockchain.BSC, + decimals = 0, + ), + NetworkItemState.Selectable( + name = "Ethereum", + protocolName = "ETH", + iconResId = R.drawable.img_polygon_22, + id = "", + onNetworkClick = { }, + blockchain = Blockchain.Ethereum, + ), + ), +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt new file mode 100644 index 0000000000..c136cb1e28 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt @@ -0,0 +1,70 @@ +package com.tangem.managetokens.presentation.common.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun SimpleSelectionBlock( + title: String, + subtitle: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + roundedCorners: Boolean = true, +) { + Column( + modifier = modifier + .then( + if (roundedCorners) { + Modifier.clip(shape = RoundedCornerShape(TangemTheme.dimens.radius16)) + } else { + Modifier + }, + ) + .background(color = TangemTheme.colors.background.action) + .clickable { onClick() } + .padding( + horizontal = TangemTheme.dimens.spacing20, + vertical = TangemTheme.dimens.spacing16, + ) + .fillMaxWidth(), + ) { + Text( + text = title, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + ) + SpacerH(height = TangemTheme.dimens.spacing4) + Text( + text = subtitle, + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + ) + } +} + +@Preview +@Composable +private fun Preview_SimpleSelectionBlock_Light() { + TangemTheme(isDark = false) { + SimpleSelectionBlock(title = "Wallet", subtitle = "Family Wallet", onClick = { }) + } +} + +@Preview +@Composable +private fun Preview_SimpleSelectionBlock_Dark() { + TangemTheme(isDark = true) { + SimpleSelectionBlock(title = "Wallet", subtitle = "Family Wallet", onClick = { }) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ChooseNetworkState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ChooseNetworkState.kt new file mode 100644 index 0000000000..df2a7f015f --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ChooseNetworkState.kt @@ -0,0 +1,11 @@ +package com.tangem.managetokens.presentation.managetokens.state + +import com.tangem.managetokens.presentation.common.state.NetworkItemState +import kotlinx.collections.immutable.ImmutableList + +internal data class ChooseNetworkState( + val nativeNetworks: ImmutableList, + val nonNativeNetworks: ImmutableList, + val onNonNativeNetworkHintClick: () -> Unit, + val onCloseChooseNetworkScreen: () -> Unit, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt index 8856ab58c5..0cfdfa16d1 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt @@ -8,7 +8,8 @@ import com.tangem.features.managetokens.impl.R data class DerivationNotificationState( val totalNeeded: Int, - val missingAddressesCount: Int, + val totalWallets: Int, + val walletsToDerive: Int, val onGenerateClick: () -> Unit, ) { val config = NotificationConfig( @@ -25,8 +26,8 @@ data class DerivationNotificationState( onClick = onGenerateClick, additionalText = pluralReference( id = R.plurals.manage_tokens_number_of_wallets_android, - count = totalNeeded, - formatArgs = wrappedList(missingAddressesCount, totalNeeded), + count = totalWallets, + formatArgs = wrappedList(walletsToDerive, totalWallets), ), ), ) diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt index ae386cc7c1..7262bbe337 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt @@ -1,5 +1,7 @@ package com.tangem.managetokens.presentation.managetokens.state +import androidx.compose.runtime.MutableState + internal sealed class TokenItemState { abstract val id: String @@ -9,11 +11,13 @@ internal sealed class TokenItemState { data class Loaded( override val id: String, val name: String, - val currencyId: String, + val currencySymbol: String, + val tokenId: String, val tokenIcon: TokenIconState, val quotes: QuotesState, val rate: String?, - val availableAction: TokenButtonType, - val onButtonClick: (String) -> Unit, + val availableAction: MutableState, + val chooseNetworkState: ChooseNetworkState, + val onButtonClick: (Loaded) -> Unit, ) : TokenItemState() } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt new file mode 100644 index 0000000000..12be7db588 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt @@ -0,0 +1,60 @@ +package com.tangem.managetokens.presentation.managetokens.state.previewdata + +import androidx.compose.runtime.mutableStateOf +import com.tangem.blockchain.common.Blockchain +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.common.state.NetworkItemState +import com.tangem.managetokens.presentation.managetokens.state.ChooseNetworkState +import kotlinx.collections.immutable.toImmutableList + +internal object ChooseNetworkStatePreviewData { + + val state = ChooseNetworkState( + nativeNetworks = nativeNetworks.toImmutableList(), + nonNativeNetworks = nonNativeNetworks.toImmutableList(), + onNonNativeNetworkHintClick = {}, + onCloseChooseNetworkScreen = {}, + ) +} + +internal val nativeNetworks = listOf( + NetworkItemState.Toggleable( + name = "Ethereum", + protocolName = "ETH", + iconResId = mutableStateOf(R.drawable.img_polygon_22), + isMainNetwork = true, + isAdded = mutableStateOf(true), + id = "", + onToggleClick = { _, _ -> }, + blockchain = Blockchain.Ethereum, + address = "", + decimals = 0, + ), +) + +internal val nonNativeNetworks = listOf( + NetworkItemState.Toggleable( + name = "Ethereum", + protocolName = "ETH", + iconResId = mutableStateOf(R.drawable.img_kusama_22), + isMainNetwork = false, + isAdded = mutableStateOf(true), + id = "", + onToggleClick = { _, _ -> }, + blockchain = Blockchain.Ethereum, + address = "", + decimals = 0, + ), + NetworkItemState.Toggleable( + name = "BNB SMART CHAIN", + protocolName = "BEP20", + iconResId = mutableStateOf(R.drawable.ic_bsc_16), + isMainNetwork = false, + isAdded = mutableStateOf(false), + id = "", + onToggleClick = { _, _ -> }, + blockchain = Blockchain.BSC, + address = "", + decimals = 0, + ), +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt index 207ea75cb8..b5d88b814a 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt @@ -4,8 +4,9 @@ import com.tangem.managetokens.presentation.managetokens.state.DerivationNotific object DerivationNotificationStatePreviewData { val state = DerivationNotificationState( - totalNeeded = 3, - missingAddressesCount = 2, + totalNeeded = 5, + totalWallets = 3, + walletsToDerive = 2, onGenerateClick = {}, ) } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt index bdfedacd3e..01ed9bdda9 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.state.previewdata +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.Color import com.tangem.managetokens.presentation.managetokens.state.* import kotlinx.collections.immutable.persistentListOf @@ -13,7 +14,8 @@ internal object TokenItemStatePreviewData { get() = TokenItemState.Loaded( id = "BTC", name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin", - currencyId = "BTC", + tokenId = "BTC", + currencySymbol = "BTC", tokenIcon = tokenIconState, quotes = QuotesState.Content( priceChange = "0.43%", @@ -21,15 +23,17 @@ internal object TokenItemStatePreviewData { chartData = persistentListOf(10f, 2f, 5f, 3f, 4f, 8f, 9f, 7f, 4f), ), rate = "31 285.72$", - availableAction = TokenButtonType.ADD, + availableAction = mutableStateOf(TokenButtonType.ADD), onButtonClick = {}, + chooseNetworkState = ChooseNetworkStatePreviewData.state, ) val loadedPriceUp: TokenItemState get() = TokenItemState.Loaded( id = "BTC", name = "Bitcoin", - currencyId = "BTC", + tokenId = "BTC", + currencySymbol = "BTC", tokenIcon = tokenIconState, quotes = QuotesState.Content( priceChange = "0.43%", @@ -37,8 +41,9 @@ internal object TokenItemStatePreviewData { chartData = persistentListOf(1f, 3f, 4f, 8f, 12f, 10f, 8f, 3f, 5f, 7f), ), rate = "31 285.72$", - availableAction = TokenButtonType.NOT_AVAILABLE, + availableAction = mutableStateOf(TokenButtonType.NOT_AVAILABLE), onButtonClick = {}, + chooseNetworkState = ChooseNetworkStatePreviewData.state, ) private val tokenIconState: TokenIconState diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt new file mode 100644 index 0000000000..ebc600073f --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt @@ -0,0 +1,205 @@ +package com.tangem.managetokens.presentation.managetokens.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.components.WarningCardTitleOnly +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.common.state.ChooseWalletState +import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData +import com.tangem.managetokens.presentation.common.ui.components.NetworkItem +import com.tangem.managetokens.presentation.common.ui.components.SimpleSelectionBlock +import com.tangem.managetokens.presentation.managetokens.state.ChooseNetworkState +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState +import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData + +@Composable +internal fun ChooseNetworkScreen( + state: TokenItemState.Loaded, + walletState: ChooseWalletState, + modifier: Modifier = Modifier, +) { + val networkState = state.chooseNetworkState + LazyColumn( + modifier = modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing16), + ) { + item { + Text( + text = stringResource(id = R.string.manage_tokens_network_selector_title), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth(), + ) + } + + when (walletState) { + is ChooseWalletState.Choose -> { + item { + SpacerH(height = TangemTheme.dimens.spacing10) + } + item { + SimpleSelectionBlock( + title = stringResource(id = R.string.manage_tokens_network_selector_wallet), + subtitle = walletState.selectedWallet?.walletName ?: "", + onClick = walletState.onChooseWalletClick, + ) + } + } + ChooseWalletState.NoSelection -> Unit + is ChooseWalletState.Warning -> { + item { + SpacerH(height = TangemTheme.dimens.spacing10) + } + item { + WarningCardTitleOnly( + title = stringResource(id = R.string.manage_tokens_wallet_support_only_one_network_title), + ) + } + } + } + + item { + SpacerH(height = TangemTheme.dimens.spacing16) + } + + item { + if (networkState.nativeNetworks.isNotEmpty()) { + NativeNetworks(networkState = networkState, tokenState = state) + } + } + + if (networkState.nonNativeNetworks.isNotEmpty()) { + item { + NonNativeNetworksHeader(networkState.onNonNativeNetworkHintClick) + } + item { + SpacerH(height = TangemTheme.dimens.spacing8) + } + item { + this@LazyColumn.NonNativeNetworks(networkState = networkState, tokenState = state) + } + } + } +} + +@Composable +private fun NativeNetworks(networkState: ChooseNetworkState, tokenState: TokenItemState.Loaded) { + Column { + Text( + text = stringResource(id = R.string.manage_tokens_network_selector_native_title), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption1, + ) + SpacerH(height = TangemTheme.dimens.spacing2) + Text( + text = stringResource(id = R.string.manage_tokens_network_selector_native_subtitle), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + ) + SpacerH(height = TangemTheme.dimens.spacing8) + + networkState.nativeNetworks.forEachIndexed { index, network -> + NetworkItem( + state = network, + tokenState = tokenState, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = networkState.nativeNetworks.lastIndex, + addDefaultPadding = false, + ), + ) + } + SpacerH(height = TangemTheme.dimens.spacing16) + } +} + +@Composable +private fun LazyListScope.NonNativeNetworks(networkState: ChooseNetworkState, tokenState: TokenItemState.Loaded) { + items( + count = networkState.nonNativeNetworks.count(), + key = { index -> networkState.nonNativeNetworks[index].id }, + ) { index -> + NetworkItem( + state = networkState.nonNativeNetworks[index], + tokenState = tokenState, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = networkState.nonNativeNetworks.lastIndex, + addDefaultPadding = false, + ), + ) + } + item { + SpacerH(height = TangemTheme.dimens.spacing16) + } +} + +@Composable +private fun NonNativeNetworksHeader(onNonNativeNetworkHintClick: () -> Unit) { + Column { + Row { + Text( + text = stringResource(id = R.string.manage_tokens_network_selector_non_native_title), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption1, + ) + SpacerW(width = TangemTheme.dimens.spacing2) + Icon( + painter = painterResource(id = R.drawable.ic_information_24), + tint = TangemTheme.colors.icon.inactive, + contentDescription = null, + modifier = Modifier + .size(TangemTheme.dimens.size16) + .clickable { onNonNativeNetworkHintClick() }, + ) + } + SpacerH(height = TangemTheme.dimens.spacing2) + Text( + text = stringResource(id = R.string.manage_tokens_network_selector_non_native_subtitle), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + ) + } +} + +@Preview +@Composable +private fun Preview_ChooseNetworkScreen_Light() { + TangemTheme(isDark = false) { + ChooseNetworkScreen( + state = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded, + walletState = ChooseWalletStatePreviewData.state, + ) + } +} + +@Preview +@Composable +private fun Preview_ChooseNetworkScreen_Dark() { + TangemTheme(isDark = true) { + ChooseNetworkScreen( + state = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded, + walletState = ChooseWalletStatePreviewData.state, + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt index 139e4f9652..12a403e3e4 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt @@ -54,7 +54,7 @@ private fun LoadedTokenItem(state: TokenItemState.Loaded, modifier: Modifier = M modifier = Modifier .weight(weight = 1f), ) { - TokenName(name = state.name, currencyId = state.currencyId) + TokenName(name = state.name, currencyId = state.currencySymbol) TokenPriceData(price = state.rate, quotesState = state.quotes) } SpacerW24() @@ -67,8 +67,8 @@ private fun LoadedTokenItem(state: TokenItemState.Loaded, modifier: Modifier = M } TokenButton( - type = state.availableAction, - onClick = { state.onButtonClick(state.currencyId) }, + type = state.availableAction.value, + onClick = { state.onButtonClick(state) }, ) } } From 2a9a80414d770ee9a218e789de1d24fe90762166 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 24 Nov 2023 16:33:38 +0300 Subject: [PATCH 02/20] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 20 +- .../appbar/AppBarWithBackButtonAndIcon.kt | 8 +- .../AmountVisualTransformation.kt | 2 +- .../transactions/TransactionDoneTitle.kt | 87 ++++ .../core/ui/utils/BigDecimalFormatter.kt | 5 + .../res/drawable/ic_empty_in_process_64.xml | 10 + core/ui/src/main/res/drawable/ic_web_24.xml | 13 + data/card/build.gradle.kts | 4 + .../card/DefaultCardSdkConfigRepository.kt | 13 + domain/card/build.gradle.kts | 4 + .../repository/CardSdkConfigRepository.kt | 10 + .../DefaultWalletManagersFacade.kt | 54 +++ .../walletmanager/WalletManagersFacade.kt | 57 ++- domain/tokens/models/build.gradle.kts | 3 + .../domain/tokens/utils/BigDecimalUtils.kt | 24 ++ domain/transaction/build.gradle.kts | 5 + .../transaction/error/SendTransactionError.kt | 10 + .../usecase/SendTransactionUseCase.kt | 61 +++ features/send/impl/build.gradle.kts | 3 + .../features/send/impl/di/SendRouterModule.kt | 5 +- .../send/impl/navigation/DefaultSendRouter.kt | 12 +- .../send/impl/navigation/InnerSendRouter.kt | 9 + .../send/impl/presentation/SendFragment.kt | 10 + .../presentation/domain/SendNotification.kt | 10 + .../presentation/state/SendStateFactory.kt | 12 +- .../impl/presentation/state/SendUiState.kt | 11 +- .../impl/presentation/state/StateRouter.kt | 55 ++- .../SendRecipientMemoFieldConverter.kt | 1 + .../presentation/ui/SendNavigationButtons.kt | 128 ++++-- .../send/impl/presentation/ui/SendScreen.kt | 14 +- .../presentation/ui/amount/AmountField.kt | 2 +- .../ui/fee/SendCustomFeeEthereum.kt | 88 ++-- .../ui/recipient/SendRecipientContent.kt | 32 +- .../ui/recipient/TextFieldWithPaste.kt | 70 ++++ .../presentation/ui/recipient/TextFields.kt | 385 ------------------ .../impl/presentation/ui/send/SendContent.kt | 193 +++++++++ .../viewmodel/MemoVerification.kt | 1 + .../viewmodel/SendClickIntents.kt | 14 + .../presentation/viewmodel/SendViewModel.kt | 128 +++++- 39 files changed, 1074 insertions(+), 499 deletions(-) rename core/ui/src/main/java/com/tangem/core/ui/components/fields/{ => visualtransformations}/AmountVisualTransformation.kt (93%) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt create mode 100644 core/ui/src/main/res/drawable/ic_empty_in_process_64.xml create mode 100644 core/ui/src/main/res/drawable/ic_web_24.xml create mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/utils/BigDecimalUtils.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/InnerSendRouter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendNotification.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 5b68512d77..8dfb19bd16 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -1,6 +1,9 @@ package com.tangem.tap.di.domain +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -15,10 +18,25 @@ internal object TransactionDomainModule { @Provides @ViewModelScoped - fun provideGetUseCase( + fun provideGetFeeUseCase( walletManagersFacade: WalletManagersFacade, dispatchers: CoroutineDispatcherProvider, ): GetFeeUseCase { return GetFeeUseCase(walletManagersFacade, dispatchers) } + + @Provides + @ViewModelScoped + fun provideSendTransactionUseCase( + isDemoCardUseCase: IsDemoCardUseCase, + walletManagersFacade: WalletManagersFacade, + cardSdkConfigRepository: CardSdkConfigRepository, + ): SendTransactionUseCase { + return SendTransactionUseCase( + isDemoCardUseCase = isDemoCardUseCase, + cardSdkConfigRepository = cardSdkConfigRepository, + walletManagersFacade = walletManagersFacade, + + ) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt index 73509da21f..6151499106 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt @@ -4,10 +4,13 @@ import androidx.annotation.DrawableRes import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.material.Icon import androidx.compose.material.Text +import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -39,7 +42,10 @@ fun AppBarWithBackButtonAndIcon( contentDescription = null, modifier = Modifier .size(size = TangemTheme.dimens.size24) - .clickable { onBackClick() }, + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + ) { onBackClick() }, tint = TangemTheme.colors.icon.primary1, ) AnimatedContent( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountVisualTransformation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt similarity index 93% rename from core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountVisualTransformation.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt index 89e4fa09ce..b3f27e218e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountVisualTransformation.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.fields +package com.tangem.core.ui.components.fields.visualtransformations import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt new file mode 100644 index 0000000000..d576eafe66 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt @@ -0,0 +1,87 @@ +package com.tangem.core.ui.components.transactions + +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.toDateFormat +import com.tangem.core.ui.utils.toTimeFormat + +/** + * Common transaction done screen title + * + * @param titleRes title resource + * @param date transaction timestamp in millis + */ +@Composable +fun TransactionDoneTitle(@StringRes titleRes: Int, date: Long, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(id = R.drawable.ic_empty_in_process_64), + contentDescription = null, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing8) + .size(TangemTheme.dimens.size64), + ) + Text( + text = stringResource(id = titleRes), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing32), + ) + Text( + text = stringResource(id = R.string.send_date_format, date.toDateFormat(), date.toTimeFormat()), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4), + ) + } +} + +// region Previews +@Preview +@Composable +private fun TransactionDoneTitlePreview_Light() { + TangemTheme { + TransactionDoneTitle( + titleRes = R.string.sent_transaction_sent_title, + date = 0, + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing16), + ) + } +} + +@Preview +@Composable +private fun TransactionDoneTitlePreview_Dark() { + TangemTheme(isDark = true) { + TransactionDoneTitle( + titleRes = R.string.sent_transaction_sent_title, + date = 0, + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing16), + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index d8e209f0ad..c64c860e28 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.utils +import com.tangem.domain.tokens.model.CryptoCurrency import java.math.BigDecimal import java.math.RoundingMode import java.text.NumberFormat @@ -24,6 +25,10 @@ object BigDecimalFormatter { return formatter.format(cryptoAmount) + "\u2009$cryptoCurrency" } + fun formatCryptoAmount(cryptoAmount: BigDecimal?, cryptoCurrency: CryptoCurrency): String { + return formatCryptoAmount(cryptoAmount, cryptoCurrency.symbol, cryptoCurrency.decimals) + } + fun formatFiatAmount( fiatAmount: BigDecimal?, fiatCurrencyCode: String, diff --git a/core/ui/src/main/res/drawable/ic_empty_in_process_64.xml b/core/ui/src/main/res/drawable/ic_empty_in_process_64.xml new file mode 100644 index 0000000000..e75b15979e --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_empty_in_process_64.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_web_24.xml b/core/ui/src/main/res/drawable/ic_web_24.xml new file mode 100644 index 0000000000..69e2fc27d9 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_web_24.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/data/card/build.gradle.kts b/data/card/build.gradle.kts index 6015c1ed3a..872f5ce751 100644 --- a/data/card/build.gradle.kts +++ b/data/card/build.gradle.kts @@ -13,6 +13,10 @@ android { dependencies { implementation(deps.androidx.datastore) + implementation(deps.tangem.blockchain) { + exclude(module = "joda-time") + } + implementation(deps.hilt.android) kapt(deps.hilt.kapt) diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt index a093b24ba0..97c5e7dc35 100644 --- a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt @@ -1,6 +1,7 @@ package com.tangem.data.card import com.tangem.TangemSdk +import com.tangem.blockchain.common.CommonSigner import com.tangem.common.UserCodeType import com.tangem.common.core.CardIdDisplayFormat import com.tangem.common.core.UserCodeRequestPolicy @@ -57,4 +58,16 @@ internal class DefaultCardSdkConfigRepository( } override fun isAccessCodeSavingEnabled(): Boolean = preferencesDataSource.shouldSaveAccessCodes + + override fun getCommonSigner(cardId: String?) = CommonSigner( + tangemSdk = sdk, + cardId = cardId, + initialMessage = null, + ) + + override fun isLinkedTerminal() = sdk.config.linkedTerminal + + override fun setLinkedTerminal(isLinked: Boolean?) { + sdk.config.linkedTerminal = isLinked + } } \ No newline at end of file diff --git a/domain/card/build.gradle.kts b/domain/card/build.gradle.kts index 71512156cd..342a410f5a 100644 --- a/domain/card/build.gradle.kts +++ b/domain/card/build.gradle.kts @@ -19,4 +19,8 @@ dependencies { implementation(deps.tangem.card.core) + implementation(deps.tangem.blockchain) { + exclude(module = "joda-time") + } + } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt index 1190a2952d..47d13a5a8c 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt @@ -1,6 +1,7 @@ package com.tangem.domain.card.repository import com.tangem.TangemSdk +import com.tangem.blockchain.common.CommonSigner import com.tangem.domain.models.scan.ProductType /** @@ -28,4 +29,13 @@ interface CardSdkConfigRepository { /** Check if access code saving is enabled */ fun isAccessCodeSavingEnabled(): Boolean + + /** Get common signer by [cardId] */ + fun getCommonSigner(cardId: String?): CommonSigner + + /** Check if linked terminal is enabled */ + fun isLinkedTerminal(): Boolean? + + /** Set linked terminal by [isLinked] */ + fun setLinkedTerminal(isLinked: Boolean?) } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 15010e8b52..df99d53034 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -10,6 +10,7 @@ import com.tangem.blockchain.blockchains.solana.RentProvider import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest import com.tangem.blockchain.extensions.Result @@ -35,6 +36,7 @@ import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import timber.log.Timber import java.math.BigDecimal +import java.util.EnumSet @Suppress("LargeClass") // FIXME: Move to its own module and make internal @@ -431,6 +433,58 @@ class DefaultWalletManagersFacade( ) } + override suspend fun validateTransaction( + amount: Amount, + fee: Amount?, + userWalletId: UserWalletId, + network: Network, + ): EnumSet? { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + return walletManager?.validateTransaction(amount, fee) + } + + override suspend fun createTransaction( + amount: Amount, + fee: Fee, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + ): TransactionData? { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + + val txData = walletManager?.createTransaction(amount, fee, destination)?.copy( + extras = null, // todo add memo [[REDACTED_JIRA]] + ) + + return txData + } + + override suspend fun sendTransaction( + txData: TransactionData, + signer: CommonSigner, + userWalletId: UserWalletId, + network: Network, + ): SimpleResult { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + return (walletManager as TransactionSender).send(txData, signer) + } + private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set) { if (tokens.isEmpty()) return diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 8e830e0bdd..f2b89b51a6 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -2,13 +2,13 @@ package com.tangem.domain.walletmanager import arrow.core.Either import com.tangem.blockchain.blockchains.solana.RentProvider -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.extensions.SimpleResult import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -19,6 +19,7 @@ import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import java.math.BigDecimal +import java.util.EnumSet // TODO: Move to its own module /** @@ -161,4 +162,54 @@ interface WalletManagersFacade { userWalletId: UserWalletId, network: Network, ): Result? + + /** + * Validates transaction + * + * @param amount of transaction + * @param fee of transaction + * @param userWalletId selected wallet id + * @param network network of currency + */ + suspend fun validateTransaction( + amount: Amount, + fee: Amount?, + userWalletId: UserWalletId, + network: Network, + ): EnumSet? + + /** + * Creates transaction [TransactionData] + * + * @param amount of transaction + * @param fee of transaction + * @param memo of transaction optional + * @param destination address + * @param userWalletId selected wallet id + * @param network network of currency + */ + @Suppress("LongParameterList") + suspend fun createTransaction( + amount: Amount, + fee: Fee, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + ): TransactionData? + + /** + * Sends transaction + * + * @param txData transaction data + * @param signer card signer + * @param userWalletId selected wallet id + * @param network network of currency + */ + suspend fun sendTransaction( + txData: TransactionData, + signer: CommonSigner, + userWalletId: UserWalletId, + network: Network, + ): SimpleResult } \ No newline at end of file diff --git a/domain/tokens/models/build.gradle.kts b/domain/tokens/models/build.gradle.kts index e9075cefa0..a354fa4700 100644 --- a/domain/tokens/models/build.gradle.kts +++ b/domain/tokens/models/build.gradle.kts @@ -12,4 +12,7 @@ android { dependencies { implementation(projects.domain.txhistory.models) implementation(projects.core.analytics.models) + implementation(deps.tangem.blockchain) { + exclude(module = "joda-time") + } } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/utils/BigDecimalUtils.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/utils/BigDecimalUtils.kt new file mode 100644 index 0000000000..fe93d1d8ec --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/utils/BigDecimalUtils.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.tokens.utils + +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Token +import com.tangem.domain.tokens.model.CryptoCurrency +import java.math.BigDecimal + +/** Converts `BigDecimal` [cryptoCurrency] to [Amount] */ +fun BigDecimal.convertToAmount(cryptoCurrency: CryptoCurrency) = Amount( + currencySymbol = cryptoCurrency.symbol, + value = this, + decimals = cryptoCurrency.decimals, + type = when (cryptoCurrency) { + is CryptoCurrency.Coin -> AmountType.Coin + is CryptoCurrency.Token -> AmountType.Token( + token = Token( + symbol = cryptoCurrency.symbol, + contractAddress = cryptoCurrency.contractAddress, + decimals = cryptoCurrency.decimals, + ), + ) + }, +) \ No newline at end of file diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index 8da795757a..571b6c0f50 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -14,10 +14,15 @@ dependencies { implementation(projects.core.utils) + /** Tangem SDKs */ + implementation(deps.tangem.card.core) implementation(deps.tangem.blockchain) + implementation(projects.domain.models) implementation(projects.domain.legacy) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.demo) + implementation(projects.domain.card) } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt new file mode 100644 index 0000000000..26fdceff5a --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.transaction.error + +sealed class SendTransactionError { + + object DemoCardError : SendTransactionError() + + data class DataError(val message: String?) : SendTransactionError() + + data class NetworkError(val message: String?) : SendTransactionError() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt new file mode 100644 index 0000000000..0a289bfcdc --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -0,0 +1,61 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet + +class SendTransactionUseCase( + private val isDemoCardUseCase: IsDemoCardUseCase, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val walletManagersFacade: WalletManagersFacade, +) { + suspend operator fun invoke( + txData: TransactionData, + userWallet: UserWallet, + network: Network, + ): Either { + val signer = cardSdkConfigRepository.getCommonSigner( + userWallet.cardId, + ) + + val linkedTerminal = cardSdkConfigRepository.isLinkedTerminal() + if (userWallet.scanResponse.card.isStart2Coin) { + cardSdkConfigRepository.setLinkedTerminal(false) + } + val sendResult = try { + if (isDemoCardUseCase(cardId = userWallet.cardId)) { + SendTransactionError.DemoCardError.left() + } else { + walletManagersFacade.sendTransaction( + txData = txData, + signer = signer, + userWalletId = userWallet.walletId, + network = network, + ).right() + } + } catch (ex: Exception) { + cardSdkConfigRepository.setLinkedTerminal(linkedTerminal) + SendTransactionError.DataError(ex.message).left() + } + + cardSdkConfigRepository.setLinkedTerminal(linkedTerminal) + return sendResult.fold( + ifRight = { result -> + when (result) { + is SimpleResult.Success -> true.right() + is SimpleResult.Failure -> SendTransactionError.NetworkError(result.error.message).left() + } + }, + ifLeft = { it.left() }, + ) + } +} \ No newline at end of file diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index bb59a8e295..1af2bafaee 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(projects.core.featuretoggles) implementation(projects.core.ui) implementation(projects.core.utils) + implementation(projects.core.navigation) /** Domain modules */ implementation(projects.domain.models) @@ -58,6 +59,8 @@ dependencies { implementation(projects.domain.txhistory) implementation(projects.domain.txhistory.models) implementation(projects.domain.transaction) + implementation(projects.domain.card) + implementation(projects.domain.demo) /** Feature modules */ implementation(projects.features.send.api) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt index 8bf19609d8..7f38cacd35 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.di +import com.tangem.core.navigation.ReduxNavController import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.DefaultSendRouter import dagger.Module @@ -17,7 +18,7 @@ internal object SendRouterModule { @Provides @ActivityScoped - fun provideSendRouter(): SendRouter { - return DefaultSendRouter() + fun provideSendRouter(reduxNavController: ReduxNavController): SendRouter { + return DefaultSendRouter(reduxNavController) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt index 4db1a5a674..c72ca92ae7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt @@ -1,9 +1,17 @@ package com.tangem.features.send.impl.navigation import androidx.fragment.app.Fragment -import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.core.navigation.NavigationAction +import com.tangem.core.navigation.ReduxNavController import com.tangem.features.send.impl.presentation.SendFragment -internal class DefaultSendRouter : SendRouter { +internal class DefaultSendRouter( + private val reduxNavController: ReduxNavController, +) : InnerSendRouter { + override fun getEntryFragment(): Fragment = SendFragment.create() + + override fun openUrl(url: String) { + reduxNavController.navigate(NavigationAction.OpenUrl(url = url)) + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/InnerSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/InnerSendRouter.kt new file mode 100644 index 0000000000..b7cd7f934a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/InnerSendRouter.kt @@ -0,0 +1,9 @@ +package com.tangem.features.send.impl.navigation + +import com.tangem.features.send.api.navigation.SendRouter + +interface InnerSendRouter : SendRouter { + + /** Open website by [url] */ + fun openUrl(url: String) +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index 2790caab93..cd47d6c8fe 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -8,6 +8,8 @@ import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.core.ui.theme.AppThemeModeHolder +import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.ui.SendScreen import com.tangem.features.send.impl.presentation.viewmodel.SendViewModel @@ -24,12 +26,20 @@ internal class SendFragment : ComposeFragment() { @Inject override lateinit var appThemeModeHolder: AppThemeModeHolder + @Inject + lateinit var router: SendRouter + private val viewModel by viewModels() + private val innerSendRouter: InnerSendRouter + get() = requireNotNull(router as? InnerSendRouter) { + "innerSendRouter should be instance of InnerSendRouter" + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) viewModel.setRouter( + innerSendRouter, StateRouter( fragmentManager = WeakReference(parentFragmentManager), ), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendNotification.kt new file mode 100644 index 0000000000..720270d86d --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendNotification.kt @@ -0,0 +1,10 @@ +package com.tangem.features.send.impl.presentation.domain + +sealed class SendNotification { + + sealed class Info(val message: String) : SendNotification() + + sealed class Critical(val message: String) : SendNotification() + + sealed class Error(val message: String) : SendNotification() +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 38583d0676..bae35722ae 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -86,6 +86,7 @@ internal class SendStateFactory( amountState = amountStateConverter.convert(Unit), recipientState = recipientStateConverter.convert(Unit), feeState = feeStateConverter.convert(Unit), + sendState = SendStates.SendState(), ) //endregion @@ -122,15 +123,15 @@ internal class SendStateFactory( val recipientState = state.recipientState ?: return state val isValidMemo = validateMemo( - memo = value, + memo = recipientState.addressTextField.value.value, cryptoCurrency = cryptoCurrencyStatusProvider().currency, ) val isAddressInWallet = isNotAddressInWallet( + address = value, walletAddresses = walletAddressesProvider(), - address = recipientState.addressTextField.value.value, ) val isValidAddress = verifyAddress( - address = recipientState.addressTextField.value.value, + address = value, cryptoCurrency = cryptoCurrencyStatusProvider().currency, ) @@ -138,8 +139,7 @@ internal class SendStateFactory( it.copy( value = value, error = when { - !isValidAddress -> TextReference.Res(R.string.send_recipient_address_error) - !isAddressInWallet -> TextReference.Res(R.string.send_recipient_address_error) + !isValidAddress || !isAddressInWallet -> TextReference.Res(R.string.send_recipient_address_error) else -> null }, isError = !isValidAddress || !isAddressInWallet, @@ -169,11 +169,9 @@ internal class SendStateFactory( cryptoCurrency = cryptoCurrencyStatusProvider().currency, ) - // todo add memo validation error text recipientState.memoTextField?.update { it.copy( value = value, - error = TextReference.Res(R.string.send_memo_destination_tag_error), isError = !isValidMemo, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index 207e63064d..1d7474d6b4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -23,6 +23,7 @@ internal data class SendUiState( val amountState: SendStates.AmountState? = null, val recipientState: SendStates.RecipientState? = null, val feeState: SendStates.FeeState? = null, + val sendState: SendStates.SendState? = null, val recipientList: MutableStateFlow> = MutableStateFlow(PagingData.empty()), val currentState: MutableStateFlow, ) @@ -65,11 +66,14 @@ internal sealed class SendStates { val receivedAmount: MutableStateFlow = MutableStateFlow(""), ) : SendStates() - // todo [REDACTED_JIRA] /** Send state */ data class SendState( - val isSuccess: Boolean, - ) + override val type: SendUiStateType = SendUiStateType.Send, + val isSending: MutableStateFlow = MutableStateFlow(false), + val isSuccess: MutableStateFlow = MutableStateFlow(false), + val transactionDate: MutableStateFlow = MutableStateFlow(0L), + val txUrl: MutableStateFlow = MutableStateFlow(""), + ) : SendStates() } enum class SendUiStateType { @@ -77,5 +81,4 @@ enum class SendUiStateType { Recipient, Fee, Send, - Done, } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt index e50ae05ae8..e92791bba9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt @@ -9,28 +9,61 @@ internal class StateRouter( private val fragmentManager: WeakReference, ) { var currentState: MutableStateFlow = MutableStateFlow(SendUiStateType.Amount) + private set + + private var isFromSend: Boolean = false + + fun popBackStack() { + fragmentManager.get()?.popBackStack() + } fun onBackClick() { - fragmentManager.get()?.popBackStack() + if (isFromSend) { + showSend() + } else { + when (currentState.value) { + SendUiStateType.Amount -> popBackStack() + SendUiStateType.Recipient -> showAmount() + SendUiStateType.Fee -> showRecipient() + SendUiStateType.Send -> showFee() + } + } } fun onNextClick() { when (currentState.value) { - SendUiStateType.Amount -> currentState.update { SendUiStateType.Recipient } - SendUiStateType.Recipient -> currentState.update { SendUiStateType.Fee } - SendUiStateType.Fee -> currentState.update { SendUiStateType.Send } - SendUiStateType.Send -> currentState.update { SendUiStateType.Done } - SendUiStateType.Done -> onBackClick() + SendUiStateType.Amount -> showRecipient() + SendUiStateType.Recipient -> showFee() + SendUiStateType.Fee -> showSend() + SendUiStateType.Send -> onBackClick() } } fun onPrevClick() { when (currentState.value) { - SendUiStateType.Amount -> onBackClick() - SendUiStateType.Recipient -> currentState.update { SendUiStateType.Amount } - SendUiStateType.Fee -> currentState.update { SendUiStateType.Recipient } - SendUiStateType.Send -> currentState.update { SendUiStateType.Fee } - SendUiStateType.Done -> onBackClick() + SendUiStateType.Amount -> popBackStack() + SendUiStateType.Recipient -> showAmount() + SendUiStateType.Fee -> showRecipient() + SendUiStateType.Send -> popBackStack() } } + + fun showAmount(isFromSend: Boolean = false) { + this.isFromSend = isFromSend + currentState.update { SendUiStateType.Amount } + } + + fun showRecipient(isFromSend: Boolean = false) { + this.isFromSend = isFromSend + currentState.update { SendUiStateType.Recipient } + } + + fun showFee(isFromSend: Boolean = false) { + this.isFromSend = isFromSend + currentState.update { SendUiStateType.Fee } + } + + fun showSend() { + currentState.update { SendUiStateType.Send } + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt index d1890038cb..a1c0bd5603 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt @@ -45,6 +45,7 @@ internal class SendRecipientMemoFieldConverter( ), placeholder = TextReference.Res(R.string.send_optional_field), label = TextReference.Res(value), + error = TextReference.Res(R.string.send_memo_destination_tag_error), ), ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 30ca727485..e7c0df4249 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -1,23 +1,30 @@ package com.tangem.features.send.impl.presentation.ui +import androidx.annotation.StringRes import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.R -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.* +import com.tangem.core.ui.extensions.shareText import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType @@ -64,16 +71,16 @@ private fun SendSecondaryNavigationButton(uiState: SendUiState) { @Composable private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier = Modifier) { - val currentState = uiState.currentState.collectAsState() + val currentState = uiState.currentState.collectAsStateWithLifecycle() + val isSuccess = uiState.sendState?.isSuccess?.collectAsStateWithLifecycle()?.value ?: false + val isSending = uiState.sendState?.isSending?.collectAsStateWithLifecycle()?.value ?: false + val txUrl = uiState.sendState?.txUrl?.collectAsStateWithLifecycle()?.value.orEmpty() - val buttonTextId = when (currentState.value) { - SendUiStateType.Amount, - SendUiStateType.Recipient, - SendUiStateType.Fee, - -> R.string.common_next - SendUiStateType.Send -> R.string.common_send - else -> R.string.common_close - } + val (buttonTextId, buttonClick) = getButtonData( + currentState = currentState, + isSuccess = isSuccess, + uiState = uiState, + ) val isButtonEnabled = when (currentState.value) { SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false @@ -86,19 +93,92 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier label = "Update send screen state", modifier = modifier, ) { textId -> - if (currentState.value == SendUiStateType.Send) { - PrimaryButtonIconEnd( - text = stringResource(textId), - iconResId = R.drawable.ic_tangem_24, - enabled = isButtonEnabled, - onClick = uiState.clickIntents::onNextClick, - ) - } else { - PrimaryButton( - text = stringResource(textId), - enabled = isButtonEnabled, - onClick = uiState.clickIntents::onNextClick, - ) + when { + currentState.value == SendUiStateType.Send && !isSuccess -> { + PrimaryButtonIconEnd( + text = stringResource(textId), + iconResId = R.drawable.ic_tangem_24, + enabled = isButtonEnabled, + onClick = buttonClick, + showProgress = isSending, + ) + } + currentState.value == SendUiStateType.Send && isSuccess -> { + PrimaryButtonsDone( + textRes = textId, + txUrl = txUrl, + onExploreClick = { uiState.clickIntents.onExploreClick(txUrl) }, + onDoneClick = buttonClick, + modifier = Modifier, + ) + } + else -> { + PrimaryButton( + text = stringResource(textId), + enabled = isButtonEnabled, + onClick = buttonClick, + ) + } } } +} + +@Composable +private fun PrimaryButtonsDone( + @StringRes textRes: Int, + txUrl: String, + onExploreClick: () -> Unit, + onDoneClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val hapticFeedback = LocalHapticFeedback.current + val context = LocalContext.current + + Column(modifier = modifier) { + if (txUrl.isNotBlank()) { + Row { + SecondaryButtonIconStart( + text = stringResource(id = R.string.common_explore), + iconResId = R.drawable.ic_web_24, + onClick = onExploreClick, + modifier = Modifier.weight(1f), + ) + SpacerW12() + SecondaryButtonIconStart( + text = stringResource(id = R.string.common_share), + iconResId = R.drawable.ic_share_24, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + context.shareText(txUrl) + }, + modifier = Modifier.weight(1f), + ) + } + SpacerH12() + } + PrimaryButton( + text = stringResource(id = textRes), + enabled = true, + onClick = onDoneClick, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +private fun getButtonData( + uiState: SendUiState, + currentState: State, + isSuccess: Boolean, +): Pair Unit> { + return when (currentState.value) { + SendUiStateType.Amount, + SendUiStateType.Recipient, + SendUiStateType.Fee, + -> R.string.common_next to uiState.clickIntents::onNextClick + SendUiStateType.Send -> if (isSuccess) { + R.string.common_close + } else { + R.string.common_send + } to uiState.clickIntents::onSendClick + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index c8f289df54..1c73c25a04 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -22,11 +22,13 @@ import com.tangem.features.send.impl.presentation.state.SendUiStateType import com.tangem.features.send.impl.presentation.ui.amount.SendAmountContent import com.tangem.features.send.impl.presentation.ui.fee.SendSpeedAndFeeContent import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent +import com.tangem.features.send.impl.presentation.ui.send.SendContent @Composable internal fun SendScreen(uiState: SendUiState) { val currentState = uiState.currentState.collectAsStateWithLifecycle() - BackHandler { uiState.clickIntents.onPrevClick() } + val isSuccess = uiState.sendState?.isSuccess?.collectAsStateWithLifecycle() + BackHandler { uiState.clickIntents.onBackClick() } Column( modifier = Modifier .fillMaxSize() @@ -36,12 +38,10 @@ internal fun SendScreen(uiState: SendUiState) { horizontalAlignment = Alignment.CenterHorizontally, ) { val titleRes = when (currentState.value) { - SendUiStateType.Amount, - SendUiStateType.Send, - -> R.string.common_send + SendUiStateType.Amount -> R.string.common_send SendUiStateType.Recipient -> R.string.send_recipient SendUiStateType.Fee -> R.string.common_fee_selector_title - SendUiStateType.Done -> null + SendUiStateType.Send -> if (isSuccess?.value == false) R.string.common_send else null } val iconRes = when (currentState.value) { SendUiStateType.Amount, @@ -52,7 +52,7 @@ internal fun SendScreen(uiState: SendUiState) { AppBarWithBackButtonAndIcon( text = titleRes?.let { stringResource(it) }, - onBackClick = uiState.clickIntents::onBackClick, + onBackClick = uiState.clickIntents::popBackStack, onIconClick = uiState.clickIntents::onQrCodeScanClick, backIconRes = R.drawable.ic_close_24, iconRes = iconRes, @@ -94,7 +94,7 @@ private fun SendScreenContent( uiState.feeState, uiState.clickIntents, ) - else -> { /* [REDACTED_TODO_COMMENT]*/ } + SendUiStateType.Send -> SendContent(uiState) } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt index 2fedacc5c1..f6471c020d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.text.style.TextAlign -import com.tangem.core.ui.components.fields.AmountVisualTransformation +import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt index a96d8c5f1b..98b24927f0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt @@ -1,17 +1,21 @@ package com.tangem.features.send.impl.presentation.ui.fee +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.fields.AmountVisualTransformation +import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation +import com.tangem.core.ui.components.inputrow.InputRowEnter +import com.tangem.core.ui.components.inputrow.InputRowEnterInfo +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.state.fields.SendTextField -import com.tangem.features.send.impl.presentation.ui.recipient.TextFieldWithInfo +import com.tangem.features.send.impl.presentation.ui.common.FooterContainer private const val ETHEREUM_UNIT = "GWEI" @@ -22,42 +26,66 @@ internal fun SendCustomFeeEthereum( symbol: String, modifier: Modifier = Modifier, ) { - val fee = customValues.value[0] - val gasPrice = customValues.value[1] - val gasLimit = customValues.value[2] - if (selectedFee == FeeType.CUSTOM && customValues.value.isNotEmpty()) { + val fee = customValues.value[0] + val gasPrice = customValues.value[1] + val gasLimit = customValues.value[2] + Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), modifier = modifier, ) { - TextFieldWithInfo( - value = fee.value, - label = stringResource(R.string.send_max_fee), + FooterContainer( footer = stringResource(R.string.send_max_fee_footer), - info = fee.label, - visualTransformation = AmountVisualTransformation(symbol), - keyboardOptions = fee.keyboardOptions, - onValueChange = fee.onValueChange, - isSingleLine = true, - ) - TextFieldWithInfo( - value = gasPrice.value, - label = stringResource(R.string.send_gas_price), + ) { + InputRowEnterInfo( + text = fee.value, + title = TextReference.Res(R.string.send_max_fee), + info = fee.label, + visualTransformation = AmountVisualTransformation(symbol), + keyboardOptions = fee.keyboardOptions, + onValueChange = fee.onValueChange, + isSingleLine = true, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } + FooterContainer( footer = stringResource(R.string.send_gas_price_footer), - onValueChange = gasPrice.onValueChange, - visualTransformation = AmountVisualTransformation(ETHEREUM_UNIT), - keyboardOptions = fee.keyboardOptions, - isSingleLine = true, - ) - TextFieldWithInfo( - value = gasLimit.value, - label = stringResource(R.string.send_gas_limit), + ) { + InputRowEnter( + text = gasPrice.value, + title = TextReference.Res(R.string.send_gas_price), + onValueChange = gasPrice.onValueChange, + visualTransformation = AmountVisualTransformation(ETHEREUM_UNIT), + keyboardOptions = fee.keyboardOptions, + isSingleLine = true, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } + FooterContainer( footer = stringResource(R.string.send_gas_limit_footer), - onValueChange = gasLimit.onValueChange, - keyboardOptions = fee.keyboardOptions, - isSingleLine = true, - ) + ) { + InputRowEnter( + text = gasLimit.value, + title = TextReference.Res(R.string.send_gas_limit), + onValueChange = gasLimit.onValueChange, + keyboardOptions = fee.keyboardOptions, + isSingleLine = true, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index 30563155b4..24319091e1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -18,11 +18,13 @@ import androidx.compose.ui.res.stringResource import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.itemContentType import androidx.paging.compose.itemKey +import com.tangem.core.ui.components.inputrow.InputRowRecipient import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.ui.common.FooterContainer import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY" @@ -45,18 +47,26 @@ internal fun SendRecipientContent( .padding(horizontal = TangemTheme.dimens.spacing16), ) { item(key = ADDRESS_FIELD_KEY) { - TextFieldWithPasteAndIcon( - value = address.value, - label = address.label, - placeholder = address.placeholder, + FooterContainer( footer = stringResource(R.string.send_recipient_address_footer, uiState.network), - onValueChange = address.onValueChange, - onPasteClick = clickIntents::onRecipientAddressValueChange, - singleLine = true, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing4), - isError = address.isError, - error = address.error, - ) + ) { + InputRowRecipient( + value = address.value, + title = address.label, + placeholder = address.placeholder, + onValueChange = address.onValueChange, + onPasteClick = clickIntents::onRecipientAddressValueChange, + singleLine = true, + isError = address.isError, + error = address.error, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4) + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } } memo?.let { memoField -> item(key = MEMO_FIELD_KEY) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt new file mode 100644 index 0000000000..8b3c83bfb0 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt @@ -0,0 +1,70 @@ +package com.tangem.features.send.impl.presentation.ui.recipient + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.fields.SimpleTextField +import com.tangem.core.ui.components.inputrow.inner.PasteButton +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.ui.common.FooterContainer + +@Composable +internal fun TextFieldWithPaste( + value: String, + placeholder: TextReference, + label: TextReference, + onValueChange: (String) -> Unit, + onPasteClick: (String) -> Unit, + modifier: Modifier = Modifier, + footer: String? = null, + error: TextReference? = null, + isError: Boolean = false, +) { + val (title, color) = if (isError && error != null) { + error to TangemTheme.colors.text.warning + } else { + label to TangemTheme.colors.text.secondary + } + FooterContainer(modifier, footer) { + Row( + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.body2, + color = color, + ) + SimpleTextField( + value = value, + placeholder = placeholder, + onValueChange = onValueChange, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing6), + ) + } + PasteButton( + isPasteButtonVisible = value.isBlank(), + onClick = onPasteClick, + modifier = Modifier + .align(CenterVertically) + .padding(end = TangemTheme.dimens.spacing16), + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt deleted file mode 100644 index f2a792f03e..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt +++ /dev/null @@ -1,385 +0,0 @@ -package com.tangem.features.send.impl.presentation.ui.recipient - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.ripple.rememberRipple -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Alignment.Companion.CenterVertically -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SpacerH8 -import com.tangem.core.ui.components.icons.identicon.IdentIcon -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.impl.R -import com.tangem.features.send.impl.presentation.ui.common.FooterContainer - -@Composable -internal fun TextFieldWithPasteAndIcon( - value: String, - placeholder: TextReference, - label: TextReference, - onValueChange: (String) -> Unit, - onPasteClick: (String) -> Unit, - modifier: Modifier = Modifier, - footer: String? = null, - singleLine: Boolean = false, - error: TextReference? = null, - isError: Boolean = false, -) { - val (title, color) = if (isError && error != null) { - error to TangemTheme.colors.text.warning - } else { - label to TangemTheme.colors.text.secondary - } - FooterContainer(modifier, footer) { - Column( - modifier = Modifier - .fillMaxWidth() - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ), - ) { - Text( - text = title.resolveReference(), - style = TangemTheme.typography.body2, - color = color, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing12, - ), - ) - Row { - IdentIcon( - address = value, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing16, - top = TangemTheme.dimens.spacing8, - bottom = TangemTheme.dimens.spacing10, - ) - .clip(RoundedCornerShape(TangemTheme.dimens.radius20)) - .size(TangemTheme.dimens.size40) - .background(TangemTheme.colors.background.tertiary), - ) - SimpleTextField( - value = value, - placeholder = placeholder, - onValueChange = onValueChange, - singleLine = singleLine, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing8, - bottom = TangemTheme.dimens.spacing10, - ) - .weight(1f) - .align(CenterVertically), - ) - PasteButton( - isPasteButtonVisible = value.isBlank(), - onClick = onPasteClick, - modifier = Modifier - .align(CenterVertically) - .padding( - start = TangemTheme.dimens.spacing4, - end = TangemTheme.dimens.spacing16, - ), - ) - } - } - } -} - -@Composable -internal fun TextFieldWithPaste( - value: String, - placeholder: TextReference, - label: TextReference, - onValueChange: (String) -> Unit, - onPasteClick: (String) -> Unit, - modifier: Modifier = Modifier, - footer: String? = null, - error: TextReference? = null, - isError: Boolean = false, -) { - val (title, color) = if (isError && error != null) { - error to TangemTheme.colors.text.warning - } else { - label to TangemTheme.colors.text.secondary - } - FooterContainer(modifier, footer) { - Row( - modifier = Modifier - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ), - ) { - Column( - modifier = Modifier - .weight(1f) - .padding(TangemTheme.dimens.spacing12), - ) { - Text( - text = title.resolveReference(), - style = TangemTheme.typography.body2, - color = color, - ) - SimpleTextField( - value = value, - placeholder = placeholder, - onValueChange = onValueChange, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing6), - ) - } - PasteButton( - isPasteButtonVisible = value.isBlank(), - onClick = onPasteClick, - modifier = Modifier - .align(CenterVertically) - .padding(end = TangemTheme.dimens.spacing16), - ) - } - } -} - -@Composable -internal fun TextFieldWithInfo( - value: String, - label: String, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, - info: TextReference? = null, - footer: String? = null, - isSingleLine: Boolean = false, - visualTransformation: VisualTransformation = VisualTransformation.None, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, -) { - FooterContainer( - footer = footer, - footerTopPadding = TangemTheme.dimens.spacing6, - modifier = modifier, - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - top = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing14, - ), - ) { - Text( - text = label, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - ) - Row { - SimpleTextField( - value = value, - onValueChange = onValueChange, - visualTransformation = visualTransformation, - singleLine = isSingleLine, - keyboardOptions = keyboardOptions, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing6) - .weight(1f), - ) - info?.let { - Text( - text = it.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing8) - .align(Alignment.Bottom), - ) - } - } - } - } -} - -@Composable -private fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) { - val clipboardManager = LocalClipboardManager.current - val hapticFeedback = LocalHapticFeedback.current - - if (isPasteButtonVisible) { - Box(modifier = modifier) { - Text( - text = "Paste", - style = TangemTheme.typography.button, - color = TangemTheme.colors.text.primary2, - modifier = Modifier - .background( - color = TangemTheme.colors.button.primary, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) - .padding( - horizontal = TangemTheme.dimens.spacing10, - vertical = TangemTheme.dimens.spacing2, - ) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(radius = TangemTheme.dimens.radius8), - onClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - onClick( - clipboardManager - .getText() - ?.toString() - .orEmpty(), - ) - }, - ), - ) - } - } else { - Icon( - painter = painterResource(id = R.drawable.ic_close_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = stringResource(R.string.common_close), - modifier = modifier - .size(TangemTheme.dimens.size20) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(radius = TangemTheme.dimens.radius10), - onClick = { onClick("") }, - ), - ) - } -} - -@Composable -private fun SimpleTextField( - value: String, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, - placeholder: TextReference? = null, - singleLine: Boolean = false, - visualTransformation: VisualTransformation = VisualTransformation.None, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, -) { - val focusRequester = remember { FocusRequester() } - BasicTextField( - value = value, - onValueChange = onValueChange, - textStyle = TangemTheme.typography.body2.copy(color = TangemTheme.colors.text.primary1), - cursorBrush = SolidColor(TangemTheme.colors.text.primary1), - singleLine = singleLine, - visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions, - decorationBox = { textValue -> - Box { - if (value.isBlank() && placeholder != null) { - Text( - text = placeholder.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.disabled, - modifier = Modifier, - ) - } - textValue() - } - }, - modifier = modifier - .focusRequester(focusRequester), - ) -} - -//region preview -@Preview -@Composable -private fun TextFieldPreview_Light() { - TangemTheme { - Column { - TextFieldWithPaste( - value = "", - label = TextReference.Res(R.string.send_recipient), - placeholder = TextReference.Res(R.string.send_enter_address_field), - onValueChange = {}, - onPasteClick = {}, - ) - SpacerH8() - TextFieldWithPasteAndIcon( - value = "", - label = TextReference.Res(R.string.send_extras_hint_memo), - placeholder = TextReference.Res(R.string.send_optional_field), - onValueChange = {}, - onPasteClick = {}, - ) - SpacerH8() - TextFieldWithInfo( - value = "Text", - label = stringResource(R.string.send_extras_hint_memo), - info = TextReference.Res(R.string.send_optional_field), - footer = stringResource(R.string.send_max_fee), - onValueChange = {}, - ) - } - } -} - -@Preview -@Composable -private fun TextFieldPreview_Dark() { - TangemTheme(isDark = true) { - Column { - TextFieldWithPaste( - value = "", - label = TextReference.Res(R.string.send_recipient), - placeholder = TextReference.Res(R.string.send_enter_address_field), - onValueChange = {}, - onPasteClick = {}, - ) - SpacerH8() - TextFieldWithPasteAndIcon( - value = "", - label = TextReference.Res(R.string.send_extras_hint_memo), - placeholder = TextReference.Res(R.string.send_optional_field), - onValueChange = {}, - onPasteClick = {}, - ) - SpacerH8() - TextFieldWithInfo( - value = "Text", - label = stringResource(R.string.send_extras_hint_memo), - info = TextReference.Res(R.string.send_optional_field), - footer = stringResource(R.string.send_max_fee), - onValueChange = {}, - ) - } - } -} -//endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt new file mode 100644 index 0000000000..037913ef69 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -0,0 +1,193 @@ +package com.tangem.features.send.impl.presentation.ui.send + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.extensions.toBigDecimalOrDefault +import com.tangem.core.ui.components.inputrow.InputRowDefault +import com.tangem.core.ui.components.inputrow.InputRowImage +import com.tangem.core.ui.components.inputrow.InputRowRecipientDefault +import com.tangem.core.ui.components.transactions.TransactionDoneTitle +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType + +@Suppress("LongMethod") +@Composable +internal fun SendContent(uiState: SendUiState) { + val amountState = uiState.amountState ?: return + val recipientState = uiState.recipientState ?: return + val feeState = uiState.feeState ?: return + val sendState = uiState.sendState ?: return + + val isSuccess = sendState.isSuccess.collectAsStateWithLifecycle() + val timestamp = sendState.transactionDate.collectAsStateWithLifecycle() + + Column( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + AnimatedVisibility(visible = isSuccess.value) { + TransactionDoneTitle( + titleRes = R.string.sent_transaction_sent_title, + date = timestamp.value, + ) + } + AnimatedVisibility(visible = !isSuccess.value) { + FromWallet( + walletName = amountState.walletName, + walletBalance = amountState.walletBalance, + ) + } + AmountBlock( + amountState = amountState, + isSuccess = isSuccess, + onClick = uiState.clickIntents::showAmount, + ) + RecipientBlock( + recipientState = recipientState, + isSuccess = isSuccess, + onClick = uiState.clickIntents::showRecipient, + ) + FeeBlock( + feeState = feeState, + isSuccess = isSuccess, + onClick = uiState.clickIntents::showFee, + ) + } +} + +@Composable +private fun FromWallet(walletName: String, walletBalance: String) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.button.disabled) + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = buildAnnotatedString { + append(stringResource(R.string.send_from_wallet_android)) + append(" ") + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(walletName) + } + }, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + Text( + text = walletBalance, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing8, + ), + ) + } +} + +@Composable +private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: State, onClick: () -> Unit) { + val amount = amountState.amountTextField.collectAsStateWithLifecycle() + + val cryptoAmount = formatCryptoAmount( + cryptoCurrency = amountState.cryptoCurrencyStatus.currency, + cryptoAmount = amount.value.value.toBigDecimalOrDefault(), + ) + val fiatAmount = BigDecimalFormatter.formatFiatAmount( + fiatAmount = amount.value.fiatValue.toBigDecimalOrDefault(), + fiatCurrencyCode = amountState.appCurrency.code, + fiatCurrencySymbol = amountState.appCurrency.symbol, + ) + InputRowImage( + title = TextReference.Res(R.string.send_amount_label), + subtitle = TextReference.Str(cryptoAmount), + caption = TextReference.Str(fiatAmount), + tokenIconState = amountState.tokenIconState, + showNetworkIcon = true, + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable(enabled = !isSuccess.value) { onClick() }, + ) +} + +@Composable +private fun RecipientBlock(recipientState: SendStates.RecipientState, isSuccess: State, onClick: () -> Unit) { + val address = recipientState.addressTextField.collectAsStateWithLifecycle() + val memo = recipientState.memoTextField?.collectAsStateWithLifecycle() + + Column( + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable(enabled = !isSuccess.value) { onClick() }, + ) { + val showMemo = memo != null && memo.value.value.isNotBlank() + InputRowRecipientDefault( + title = TextReference.Res(R.string.send_recipient), + value = address.value.value, + showDivider = showMemo, + ) + if (showMemo) { + InputRowDefault( + title = TextReference.Res(R.string.send_extras_hint_memo), + text = TextReference.Str(memo?.value?.value.orEmpty()), + ) + } + } +} + +@Composable +private fun FeeBlock(feeState: SendStates.FeeState, isSuccess: State, onClick: () -> Unit) { + val feeSelector = + feeState.feeSelectorState.collectAsStateWithLifecycle().value as? FeeSelectorState.Content ?: return + val customValue = feeSelector.customValues.collectAsStateWithLifecycle().value.getOrNull(0) + + val feeValue = formatCryptoAmount( + cryptoCurrency = feeState.cryptoCurrencyStatus.currency, + cryptoAmount = when (val selectedFee = feeSelector.fees) { + is TransactionFee.Single -> selectedFee.normal.amount.value + is TransactionFee.Choosable -> when (feeSelector.selectedFee) { + FeeType.SLOW -> selectedFee.minimum.amount.value + FeeType.MARKET -> selectedFee.normal.amount.value + FeeType.FAST -> selectedFee.priority.amount.value + FeeType.CUSTOM -> customValue?.value.toBigDecimalOrDefault() + } + }, + ) + InputRowDefault( + title = TextReference.Res(R.string.send_network_fee_title), + text = TextReference.Str(feeValue), + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable(enabled = !isSuccess.value) { onClick() }, + ) +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt index a27ce13f23..ae24d705fd 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt @@ -7,6 +7,7 @@ import java.math.BigInteger internal fun validateMemo(memo: String, cryptoCurrency: CryptoCurrency?): Boolean { if (cryptoCurrency == null) return false + if (memo.isEmpty()) return true return when (cryptoCurrency.network.id.value) { Blockchain.XRP.id -> { val tag = memo.toLongOrNull() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt index 35df0a6614..d29ab95bee 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt @@ -4,6 +4,8 @@ import com.tangem.features.send.impl.presentation.state.fee.FeeType interface SendClickIntents { + fun popBackStack() + fun onBackClick() fun onNextClick() @@ -33,4 +35,16 @@ interface SendClickIntents { fun onSubtractSelect(value: Boolean) // endregion + + // region Send + fun onSendClick() + + fun showAmount() + + fun showRecipient() + + fun showFee() + + fun onExploreClick(txUrl: String) + // endregion } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 02f2caefc6..4fdc321d9f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -9,6 +9,7 @@ import arrow.core.getOrElse import com.tangem.blockchain.blockchains.xrp.XrpAddressService import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.Address +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.Provider import com.tangem.core.ui.utils.BigDecimalFormatter @@ -18,8 +19,11 @@ import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.walletmanager.WalletManagersFacade @@ -28,6 +32,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.SendStateFactory import com.tangem.features.send.impl.presentation.state.SendUiState @@ -48,7 +53,7 @@ import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates -@Suppress("LongParameterList") +@Suppress("LongParameterList", "TooManyFunctions", "LargeClass") @HiltViewModel internal class SendViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, @@ -60,6 +65,8 @@ internal class SendViewModel @Inject constructor( private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val getFeeUseCase: GetFeeUseCase, + private val sendTransactionUseCase: SendTransactionUseCase, + private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val walletManagersFacade: WalletManagersFacade, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { @@ -73,7 +80,8 @@ internal class SendViewModel @Inject constructor( private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() - private var innerRouter: StateRouter by Delegates.notNull() + private var inneRrouter: InnerSendRouter by Delegates.notNull() + private var stateRouter: StateRouter by Delegates.notNull() private val stateFactory = SendStateFactory( clickIntents = this, @@ -102,9 +110,10 @@ internal class SendViewModel @Inject constructor( getFee() } - fun setRouter(router: StateRouter) { - innerRouter = router - uiState = uiState.copy(currentState = router.currentState) + fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) { + inneRrouter = router + this.stateRouter = stateRouter + uiState = uiState.copy(currentState = stateRouter.currentState) } private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) { @@ -271,9 +280,10 @@ internal class SendViewModel @Inject constructor( } // region screen state navigation - override fun onBackClick() = innerRouter.onBackClick() - override fun onNextClick() = innerRouter.onNextClick() - override fun onPrevClick() = innerRouter.onPrevClick() + override fun popBackStack() = stateRouter.popBackStack() + override fun onBackClick() = stateRouter.onBackClick() + override fun onNextClick() = stateRouter.onNextClick() + override fun onPrevClick() = stateRouter.onPrevClick() override fun onQrCodeScanClick() { // TODO Add QR code scanning @@ -314,7 +324,7 @@ internal class SendViewModel @Inject constructor( } private fun checkIfXrpAddressValue(value: String): Boolean { - if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.first() == XRP_X_ADDRESS) { + if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.firstOrNull() == XRP_X_ADDRESS) { viewModelScope.launch(dispatchers.io) { val result = XrpAddressService.decodeXAddress(value) onRecipientAddressValueChange(result?.address.orEmpty()) @@ -382,8 +392,108 @@ internal class SendViewModel @Inject constructor( } //endregion + // region send state clicks + override fun onSendClick() { + val sendState = uiState.sendState ?: return + + if (sendState.isSuccess.value) popBackStack() + sendState.isSending.update { true } + viewModelScope.launch(dispatchers.io) { + verifyAndSendTransaction() + } + } + + private suspend fun verifyAndSendTransaction() { + val sendState = uiState.sendState ?: return + val amount = uiState.amountState?.amountTextField?.value ?: return + val recipient = uiState.recipientState?.addressTextField?.value ?: return + val feeState = uiState.feeState?.feeSelectorState?.value as? FeeSelectorState.Content ?: return + val memo = uiState.recipientState?.memoTextField?.value + val fee = getFee(feeState) ?: return + + val amountToSend = amount.value.toBigDecimal().convertToAmount(cryptoCurrency) + + // todo add notifications [[REDACTED_JIRA]] + // val transactionErrors = walletManagersFacade.validateTransaction( + // amount = amountToSend, + // fee = fee.amount, + // userWalletId = userWalletId, + // network = cryptoCurrency.network, + // ) + + val txData = walletManagersFacade.createTransaction( + amount = amountToSend, + fee = fee, + memo = memo?.value, + destination = recipient.value, + userWalletId = userWalletId, + network = cryptoCurrency.network, + ) ?: return + + sendTransactionUseCase( + txData = txData, + userWallet = userWallet, + network = cryptoCurrency.network, + ).fold( + ifLeft = { + sendState.isSending.update { false } + // todo add notifications [[REDACTED_JIRA]] + }, + ifRight = { + sendState.transactionDate.update { + txData.date?.timeInMillis ?: System.currentTimeMillis() + } + sendState.isSuccess.update { true } + sendState.txUrl.update { + getTxUrl(txData.hash.orEmpty()) + } + }, + ) + } + + private fun getFee(feeState: FeeSelectorState.Content): Fee? { + return when (val selectedFee = feeState.fees) { + is TransactionFee.Choosable -> { + when (feeState.selectedFee) { + FeeType.SLOW -> selectedFee.minimum + FeeType.MARKET -> selectedFee.normal + FeeType.FAST -> selectedFee.priority + FeeType.CUSTOM -> { + val feeAmount = feeState.customValues.value.firstOrNull()?.value + ?.let { BigDecimal(it) } ?: return null + Fee.Common(feeAmount.convertToAmount(cryptoCurrency)) + } + } + } + is TransactionFee.Single -> selectedFee.normal + } + } + + override fun showAmount() = stateRouter.showAmount(isFromSend = true) + + override fun showRecipient() = stateRouter.showRecipient(isFromSend = true) + + override fun showFee() = stateRouter.showFee(isFromSend = true) + + override fun onExploreClick(txUrl: String) = inneRrouter.openUrl(txUrl) + + private fun getTxUrl(hash: String): String { + val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value) + // TODO: Fix ton tx urls [REDACTED_TASK_KEY] + return if (blockchain == Blockchain.TON || blockchain == Blockchain.TONTestnet) { + EMPTY + } else { + getExplorerTransactionUrlUseCase( + txHash = hash, + networkId = cryptoCurrency.network.id, + ) + } + } + // endregion + companion object { private const val XRP_X_ADDRESS = 'X' private const val DEFAULT_VALUE = "0.00" + private const val EMPTY = "" } } \ No newline at end of file From d2d7dd6e71e6140740c9cc2237bb845ae87b7c09 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 12:27:10 +0800 Subject: [PATCH 03/20] Updated on 2026-08-14 --- .../presentation/wallet/state/WalletEvent.kt | 12 + .../wallet/state2/WalletScreenState.kt | 187 ++++++++++++++ .../wallet/state2/WalletStateHolderV2.kt | 54 ++++ .../WalletScreenStateTransformer.kt | 8 + .../wallet/ui/WalletEventEffect.kt | 1 + .../wallet/ui/WalletEventEffectV2.kt | 62 +++++ .../presentation/wallet/ui/WalletScreenV2.kt | 243 ++++++++++++++++++ .../wallet/ui/WalletsListEffectsV2.kt | 45 ++++ .../wallet/ui/components/WalletsList.kt | 36 +++ .../ui/components/common/WalletContent.kt | 18 ++ .../components/common/WalletNotifications.kt | 2 +- .../multicurrency/MultiCurrencyContent.kt | 41 +++ .../multicurrency/MultiCurrencyContentItem.kt | 17 ++ .../wallet/ui/utils/LazyListStateExt.kt | 24 ++ .../wallet/ui/utils/ReviewManagerRequester.kt | 56 ++++ .../ui/utils/ScrollOffsetCollectorV2.kt | 49 ++++ .../ui/utils/WalletsScrollPreviewExt.kt | 46 ++++ 17 files changed, 900 insertions(+), 1 deletion(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletScreenState.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletStateHolderV2.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletScreenStateTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffectV2.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreenV2.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffectsV2.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollectorV2.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletsScrollPreviewExt.kt 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 From 31c82b5331f8ba8438868ee610515841f3152325 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 13:22:59 +0800 Subject: [PATCH 04/20] Updated on 2026-08-14 --- .../tap/di/domain/SettingsDomainModule.kt | 12 ++++++ .../local/preferences/PreferencesKeys.kt | 2 + .../settings/DefaultSettingsRepository.kt | 19 ++++++++++ .../data/settings/di/SettingsDataModule.kt | 2 + .../settings/IsWalletsScrollPreviewEnabled.kt | 15 ++++++++ .../NeverToShowWalletsScrollPreview.kt | 17 +++++++++ .../repositories/SettingsRepository.kt | 4 ++ .../tokens/GetCryptoCurrencyActionsUseCase.kt | 37 ++++++++++--------- .../wallets/usecase/SelectWalletUseCase.kt | 17 ++++++--- 9 files changed, 102 insertions(+), 23 deletions(-) create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/IsWalletsScrollPreviewEnabled.kt create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowWalletsScrollPreview.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 17782946ce..1c596a4b4d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -91,4 +91,16 @@ internal object SettingsDomainModule { ): UpdateBalanceHidingSettingsUseCase { return UpdateBalanceHidingSettingsUseCase(balanceHidingRepository) } + + @Provides + @ViewModelScoped + fun provideSetWalletsScrollPreviewIsShown(settingsRepository: SettingsRepository): NeverToShowWalletsScrollPreview { + return NeverToShowWalletsScrollPreview(settingsRepository = settingsRepository) + } + + @Provides + @ViewModelScoped + fun provideIsWalletsScrollPreviewEnabled(settingsRepository: SettingsRepository): IsWalletsScrollPreviewEnabled { + return IsWalletsScrollPreviewEnabled(settingsRepository = settingsRepository) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index d81065c147..6eda0886c9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -32,6 +32,8 @@ object PreferencesKeys { val SELECTED_APP_CURRENCY_KEY by lazy { stringPreferencesKey(name = "selectedAppCurrency") } val BALANCE_HIDING_SETTINGS_KEY by lazy { stringPreferencesKey(name = "balanceHidingSettings") } + + val WALLETS_SCROLL_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "walletsScrollPreview") } } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index 0fed0929d9..8d3c6f18bc 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -1,16 +1,35 @@ package com.tangem.data.settings import com.tangem.data.source.preferences.PreferencesDataSource +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.store import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext internal class DefaultSettingsRepository( private val preferencesDataSource: PreferencesDataSource, + private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, ) : SettingsRepository { override suspend fun shouldShowSaveUserWalletScreen(): Boolean { return withContext(dispatchers.io) { preferencesDataSource.shouldShowSaveUserWalletScreen } } + + override suspend fun isWalletScrollPreviewEnabled(): Boolean { + return appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.WALLETS_SCROLL_PREVIEW_KEY, + default = true, + ) + } + + override suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean) { + appPreferencesStore.store( + key = PreferencesKeys.WALLETS_SCROLL_PREVIEW_KEY, + value = isEnabled, + ) + } } \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt index 254235cf84..ba83b09d4d 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt @@ -21,10 +21,12 @@ internal object SettingsDataModule { @Singleton fun provideSettingsRepository( preferencesDataSource: PreferencesDataSource, + appPreferencesStore: AppPreferencesStore, dispatchers: CoroutineDispatcherProvider, ): SettingsRepository { return DefaultSettingsRepository( preferencesDataSource = preferencesDataSource, + appPreferencesStore = appPreferencesStore, dispatchers = dispatchers, ) } diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/IsWalletsScrollPreviewEnabled.kt b/domain/settings/src/main/java/com/tangem/domain/settings/IsWalletsScrollPreviewEnabled.kt new file mode 100644 index 0000000000..fc45e34aa0 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/IsWalletsScrollPreviewEnabled.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SettingsRepository + +/** + * Checks if wallets scroll preview is enabled + * + * @property settingsRepository settings repository + * +[REDACTED_AUTHOR] + */ +class IsWalletsScrollPreviewEnabled(private val settingsRepository: SettingsRepository) { + + suspend operator fun invoke(): Boolean = settingsRepository.isWalletScrollPreviewEnabled() +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowWalletsScrollPreview.kt b/domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowWalletsScrollPreview.kt new file mode 100644 index 0000000000..4080a08bc9 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowWalletsScrollPreview.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SettingsRepository + +/** + * Never to show wallets scroll preview + * + * @property settingsRepository settings repository + * +[REDACTED_AUTHOR] + */ +class NeverToShowWalletsScrollPreview( + private val settingsRepository: SettingsRepository, +) { + + suspend operator fun invoke() = settingsRepository.setWalletScrollPreviewAvailability(isEnabled = false) +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index cdfe2702da..7dd4507554 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -3,4 +3,8 @@ package com.tangem.domain.settings.repositories interface SettingsRepository { suspend fun shouldShowSaveUserWalletScreen(): Boolean + + suspend fun isWalletScrollPreviewEnabled(): Boolean + + suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 55d6d6adf8..d5f89f57f5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -32,10 +32,7 @@ class GetCryptoCurrencyActionsUseCase( ) { @OptIn(ExperimentalCoroutinesApi::class) - suspend operator fun invoke( - userWallet: UserWallet, - cryptoCurrencyStatus: CryptoCurrencyStatus, - ): Flow { + operator fun invoke(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus): Flow { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, @@ -43,19 +40,25 @@ class GetCryptoCurrencyActionsUseCase( userWalletId = userWallet.walletId, ) val networkId = cryptoCurrencyStatus.currency.network.id - val networkFlow = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { - operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId) - } else if (!userWallet.isMultiCurrency) { - operations.getPrimaryCurrencyStatusFlow() - } else { - operations.getNetworkCoinFlow(networkId, cryptoCurrencyStatus.currency.network.derivationPath) - } - return networkFlow.mapLatest { maybeCoinStatus -> - createTokenActionsState( - userWalletId = userWallet.walletId, - coinStatus = maybeCoinStatus.getOrNull(), - cryptoCurrencyStatus = cryptoCurrencyStatus, - ) + + return flow { + val networkFlow = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId) + } else if (!userWallet.isMultiCurrency) { + operations.getPrimaryCurrencyStatusFlow() + } else { + operations.getNetworkCoinFlow(networkId, cryptoCurrencyStatus.currency.network.derivationPath) + } + + val flow = networkFlow.mapLatest { maybeCoinStatus -> + createTokenActionsState( + userWalletId = userWallet.walletId, + coinStatus = maybeCoinStatus.getOrNull(), + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) + } + + emitAll(flow) }.flowOn(dispatchers.io) } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt index 4c894e93d2..84303de548 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt @@ -2,12 +2,13 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either import arrow.core.raise.either -import com.tangem.common.doOnFailure -import com.tangem.common.doOnSuccess +import arrow.core.right +import com.tangem.common.CompletionResult import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull import com.tangem.domain.wallets.models.SelectWalletError +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId /** @@ -22,16 +23,20 @@ class SelectWalletUseCase( private val reduxStateHolder: ReduxStateHolder, ) { - suspend operator fun invoke(userWalletId: UserWalletId): Either { + suspend operator fun invoke(userWalletId: UserWalletId): Either { return either { val userWalletsListManager = ensureUserWalletListManagerNotNull( walletsStateHolder = walletsStateHolder, raise = { SelectWalletError.DataError }, ) - userWalletsListManager.select(userWalletId) - .doOnFailure { raise(SelectWalletError.UnableToSelectUserWallet) } - .doOnSuccess { reduxStateHolder.onUserWalletSelected(it) } + return when (val result = userWalletsListManager.select(userWalletId)) { + is CompletionResult.Failure -> raise(SelectWalletError.UnableToSelectUserWallet) + is CompletionResult.Success -> { + reduxStateHolder.onUserWalletSelected(result.data) + result.data.right() + } + } } } } \ No newline at end of file From 8708876ce52109d747ba0d8714702427a5284b66 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 14:17:17 +0800 Subject: [PATCH 05/20] Updated on 2026-08-14 --- .../transformers/AddWalletTransformer.kt | 23 +++ .../CloseBottomSheetTransformer.kt | 20 +++ .../transformers/DeleteWalletTransformer.kt | 31 ++++ .../InitializeWalletsTransformer.kt | 97 ++++++++++++ .../OpenBottomSheetTransformer.kt | 42 +++++ .../ReinitializeWalletTransformer.kt | 26 ++++ .../transformers/RenameWalletTransformer.kt | 28 ++++ .../transformers/ScrollToWalletTransformer.kt | 30 ++++ .../transformers/SendEventTransformer.kt | 17 +++ .../SetCryptoCurrencyActionsTransformer.kt | 81 ++++++++++ .../SetPrimaryCurrencyTransformer.kt | 47 ++++++ .../SetRefreshStateTransformer.kt | 67 ++++++++ .../SetTokenListErrorTransformer.kt | 38 +++++ .../transformers/SetTokenListTransformer.kt | 69 +++++++++ .../SetTxHistoryCountErrorTransformer.kt | 68 +++++++++ .../SetTxHistoryCountTransformer.kt | 60 ++++++++ .../SetTxHistoryItemsErrorTransformer.kt | 41 +++++ .../SetTxHistoryItemsTransformer.kt | 41 +++++ .../transformers/SetWarningsTransformer.kt | 26 ++++ .../transformers/UnlockWalletTransformer.kt | 53 +++++++ .../UpdateBalanceHidingModeTransformer.kt | 12 ++ .../UpdateWalletCardsCountTransformer.kt | 42 +++++ .../transformers/WalletStateTransformer.kt | 23 +++ .../MultiWalletCardStateConverter.kt | 64 ++++++++ .../MultiWalletCurrencyActionsConverter.kt | 96 ++++++++++++ .../SingleWalletCardStateConverter.kt | 78 ++++++++++ .../SingleWalletMarketPriceConverter.kt | 68 +++++++++ .../converter/TokenItemStateConverter.kt | 122 +++++++++++++++ .../converter/TokenListStateConverter.kt | 93 +++++++++++ .../converter/TxHistoryItemFlowConverter.kt | 144 ++++++++++++++++++ .../converter/TxHistoryItemStateConverter.kt | 109 +++++++++++++ .../state2/utils/UserWalletConverterExt.kt | 16 ++ .../wallet/state2/utils/WalletEventSender.kt | 29 ++++ .../state2/utils/WalletLoadingStateFactory.kt | 85 +++++++++++ .../intents/WalletCardClickIntents.kt | 31 ++++ .../intents/WalletClickIntentsV2.kt | 27 ++++ .../intents/WalletContentClickIntents.kt | 53 +++++++ .../WalletCurrencyActionsClickIntents.kt | 64 ++++++++ .../intents/WalletWarningsClickIntents.kt | 64 ++++++++ 39 files changed, 2125 insertions(+) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/AddWalletTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/CloseBottomSheetTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/DeleteWalletTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/InitializeWalletsTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/OpenBottomSheetTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ReinitializeWalletTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/RenameWalletTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ScrollToWalletTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SendEventTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetCryptoCurrencyActionsTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetRefreshStateTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListErrorTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountErrorTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsErrorTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetWarningsTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UnlockWalletTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateBalanceHidingModeTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateWalletCardsCountTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletStateTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCardStateConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCurrencyActionsConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletCardStateConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletMarketPriceConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenItemStateConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenListStateConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemStateConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/UserWalletConverterExt.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletEventSender.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletLoadingStateFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/AddWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/AddWalletTransformer.kt new file mode 100644 index 0000000000..437c8d5e41 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/AddWalletTransformer.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletLoadingStateFactory +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.toImmutableList + +internal class AddWalletTransformer( + private val userWallet: UserWallet, + private val clickIntents: WalletClickIntentsV2, +) : WalletScreenStateTransformer { + + private val walletLoadingStateFactory by lazy { + WalletLoadingStateFactory(clickIntents = clickIntents) + } + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + wallets = (prevState.wallets + walletLoadingStateFactory.create(userWallet)).toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/CloseBottomSheetTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/CloseBottomSheetTransformer.kt new file mode 100644 index 0000000000..37ddf91e8c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/CloseBottomSheetTransformer.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState + +internal class CloseBottomSheetTransformer(userWalletId: UserWalletId) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy(bottomSheetConfig = prevState.bottomSheetConfig?.copy(isShow = false)) + } + is WalletState.MultiCurrency.Locked -> prevState.copy(isBottomSheetShow = false) + is WalletState.SingleCurrency.Content -> { + prevState.copy(bottomSheetConfig = prevState.bottomSheetConfig?.copy(isShow = false)) + } + is WalletState.SingleCurrency.Locked -> prevState.copy(isBottomSheetShow = false) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/DeleteWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/DeleteWalletTransformer.kt new file mode 100644 index 0000000000..34b8081a57 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/DeleteWalletTransformer.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import kotlinx.collections.immutable.toImmutableList +import timber.log.Timber + +internal class DeleteWalletTransformer( + private val selectedWalletIndex: Int, + private val deletedWalletId: UserWalletId, +) : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState { + val deletedWalletState = prevState.getDeletedWalletState() + + if (deletedWalletState == null) { + Timber.e("Wallets does not contain deleted wallet") + return prevState + } + + return prevState.copy( + selectedWalletIndex = selectedWalletIndex, + wallets = (prevState.wallets - deletedWalletState).toImmutableList(), + ) + } + + private fun WalletScreenState.getDeletedWalletState(): WalletState? { + return wallets.firstOrNull { it.walletCardState.id == deletedWalletId } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/InitializeWalletsTransformer.kt new file mode 100644 index 0000000000..439a633944 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/InitializeWalletsTransformer.kt @@ -0,0 +1,97 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig +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.utils.WalletLoadingStateFactory +import com.tangem.feature.wallet.presentation.wallet.state2.utils.createStateByWalletType +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +internal class InitializeWalletsTransformer( + private val selectedWalletIndex: Int, + private val selectedWallet: UserWallet, + private val wallets: List, + private val clickIntents: WalletClickIntentsV2, +) : WalletScreenStateTransformer { + + private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory(clickIntents = clickIntents) } + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + onBackClick = clickIntents::onBackClick, + topBarConfig = createTopBarConfig(userWallet = selectedWallet), + selectedWalletIndex = selectedWalletIndex, + wallets = wallets + .map { userWallet -> + if (userWallet.isLocked) { + createLockedState(userWallet) + } else { + walletLoadingStateFactory.create(userWallet) + } + } + .toImmutableList(), + onWalletChange = clickIntents::onWalletChange, + ) + } + + private fun createTopBarConfig(userWallet: UserWallet): WalletTopBarConfig { + return WalletTopBarConfig( + onDetailsClick = if (userWallet.isLocked) { + clickIntents::onOpenUnlockWalletsBottomSheetClick + } else { + clickIntents::onDetailsClick + }, + ) + } + + private fun createLockedState(userWallet: UserWallet): WalletState { + return userWallet.createStateByWalletType( + multiCurrencyCreator = { + WalletState.MultiCurrency.Locked( + walletCardState = userWallet.toLockedWalletCardState(), + onUnlockNotificationClick = clickIntents::onOpenUnlockWalletsBottomSheetClick, + onUnlockClick = clickIntents::onUnlockWalletClick, + onScanClick = clickIntents::onScanToUnlockWalletClick, + ) + }, + singleCurrencyCreator = { + WalletState.SingleCurrency.Locked( + walletCardState = userWallet.toLockedWalletCardState(), + buttons = createDisabledButtons(), + onUnlockNotificationClick = clickIntents::onOpenUnlockWalletsBottomSheetClick, + onUnlockClick = clickIntents::onUnlockWalletClick, + onScanClick = clickIntents::onScanToUnlockWalletClick, + onExploreClick = clickIntents::onExploreClick, + ) + }, + ) + } + + private fun UserWallet.toLockedWalletCardState(): WalletCardState { + return WalletCardState.LockedContent( + id = walletId, + title = name, + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = this), + imageResId = WalletImageResolver.resolve(userWallet = this), + onRenameClick = clickIntents::onRenameClick, + onDeleteClick = clickIntents::onDeleteBeforeConfirmationClick, + ) + } + + private fun createDisabledButtons(): PersistentList { + return persistentListOf( + WalletManageButton.Buy(enabled = false, onClick = {}), + WalletManageButton.Send(enabled = false, onClick = {}), + WalletManageButton.Receive(enabled = false, onClick = {}), + WalletManageButton.Sell(enabled = false, onClick = {}), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/OpenBottomSheetTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/OpenBottomSheetTransformer.kt new file mode 100644 index 0000000000..f48797a50f --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/OpenBottomSheetTransformer.kt @@ -0,0 +1,42 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState + +internal class OpenBottomSheetTransformer( + userWalletId: UserWalletId, + private val content: TangemBottomSheetConfigContent, + private val onDismissBottomSheet: () -> Unit, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = onDismissBottomSheet, + content = content, + ), + ) + } + is WalletState.MultiCurrency.Locked -> { + prevState.copy(isBottomSheetShow = true, onBottomSheetDismiss = onDismissBottomSheet) + } + is WalletState.SingleCurrency.Content -> { + prevState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = onDismissBottomSheet, + content = content, + ), + ) + } + is WalletState.SingleCurrency.Locked -> { + prevState.copy(isBottomSheetShow = true, onBottomSheetDismiss = onDismissBottomSheet) + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ReinitializeWalletTransformer.kt new file mode 100644 index 0000000000..ad0ef39b08 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ReinitializeWalletTransformer.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletLoadingStateFactory +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.persistentListOf + +/** +[REDACTED_AUTHOR] + */ +internal class ReinitializeWalletTransformer( + private val userWallet: UserWallet, + private val clickIntents: WalletClickIntentsV2, +) : WalletScreenStateTransformer { + + private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory(clickIntents = clickIntents) } + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + wallets = persistentListOf( + walletLoadingStateFactory.create(userWallet), + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/RenameWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/RenameWalletTransformer.kt new file mode 100644 index 0000000000..80b3692e93 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/RenameWalletTransformer.kt @@ -0,0 +1,28 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import timber.log.Timber + +internal class RenameWalletTransformer( + userWalletId: UserWalletId, + private val newName: String, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy(walletCardState = prevState.walletCardState.copySealed(title = newName)) + } + is WalletState.SingleCurrency.Content -> { + prevState.copy(walletCardState = prevState.walletCardState.copySealed(title = newName)) + } + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to rename wallet in locked state") + prevState + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ScrollToWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ScrollToWalletTransformer.kt new file mode 100644 index 0000000000..17c76d98e5 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ScrollToWalletTransformer.kt @@ -0,0 +1,30 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.common.Provider +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState + +internal class ScrollToWalletTransformer( + private val index: Int, + private val currentStateProvider: Provider, + private val stateUpdater: (WalletScreenState) -> Unit, +) : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + event = triggeredEvent( + data = WalletEvent.ChangeWallet(index), + onConsume = { + stateUpdater( + currentStateProvider().copy( + selectedWalletIndex = index, + event = consumedEvent(), + ), + ) + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SendEventTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SendEventTransformer.kt new file mode 100644 index 0000000000..f34236635b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SendEventTransformer.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState + +internal class SendEventTransformer( + private val event: WalletEvent, + private val onConsume: () -> Unit, +) : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + event = triggeredEvent(data = event, onConsume = onConsume), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetCryptoCurrencyActionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetCryptoCurrencyActionsTransformer.kt new file mode 100644 index 0000000000..605e40f98a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetCryptoCurrencyActionsTransformer.kt @@ -0,0 +1,81 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList +import timber.log.Timber + +internal class SetCryptoCurrencyActionsTransformer( + private val tokenActionsState: TokenActionsState, + private val userWallet: UserWallet, + private val clickIntents: WalletClickIntentsV2, +) : WalletStateTransformer(userWallet.walletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> { + prevState.copy(buttons = tokenActionsState.toManageButtons()) + } + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to load primary currency status for locked wallet") + prevState + } + is WalletState.MultiCurrency, + -> { + Timber.e("Impossible to load crypto currency actions for multi-currency wallet") + prevState + } + } + } + + private fun TokenActionsState.toManageButtons(): PersistentList { + return states + .filterIfS2C() + .mapNotNull { action -> + when (action) { + is TokenActionsState.ActionState.Buy -> { + WalletManageButton.Buy( + enabled = action.enabled, + onClick = { clickIntents.onBuyClick(cryptoCurrencyStatus) }, + ) + } + is TokenActionsState.ActionState.Receive -> { + WalletManageButton.Receive( + enabled = action.enabled, + onClick = { clickIntents.onReceiveClick(cryptoCurrencyStatus) }, + ) + } + is TokenActionsState.ActionState.Sell -> { + WalletManageButton.Sell( + enabled = action.enabled, + onClick = { clickIntents.onSellClick(cryptoCurrencyStatus) }, + ) + } + is TokenActionsState.ActionState.Send -> { + WalletManageButton.Send( + enabled = action.enabled, + onClick = { clickIntents.onSendClick(cryptoCurrencyStatus) }, + ) + } + else -> { + null + } + } + } + .toPersistentList() + } + + private fun List.filterIfS2C(): List { + return if (userWallet.scanResponse.cardTypesResolver.isStart2Coin()) { + filterNot { it is TokenActionsState.ActionState.Buy || it is TokenActionsState.ActionState.Sell } + } else { + this + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt new file mode 100644 index 0000000000..83f89adc93 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt @@ -0,0 +1,47 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.SingleWalletCardStateConverter +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.SingleWalletMarketPriceConverter +import timber.log.Timber + +internal class SetPrimaryCurrencyTransformer( + private val userWallet: UserWallet, + private val status: CryptoCurrencyStatus.Status, + private val appCurrency: AppCurrency, +) : WalletStateTransformer(userWallet.walletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> { + prevState.copy( + walletCardState = prevState.walletCardState.toLoadedState(), + marketPriceBlockState = prevState.marketPriceBlockState.toLoadedState(), + ) + } + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to load primary currency status for locked wallet") + prevState + } + is WalletState.MultiCurrency, + -> { + Timber.e("Impossible to load primary currency status for multi-currency wallet") + prevState + } + } + } + + private fun WalletCardState.toLoadedState(): WalletCardState { + return SingleWalletCardStateConverter(status, userWallet, appCurrency).convert(value = this) + } + + private fun MarketPriceBlockState.toLoadedState(): MarketPriceBlockState { + return SingleWalletMarketPriceConverter(status, appCurrency).convert(value = this) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetRefreshStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetRefreshStateTransformer.kt new file mode 100644 index 0000000000..043c611ccd --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetRefreshStateTransformer.kt @@ -0,0 +1,67 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.mutate + +internal class SetRefreshStateTransformer( + userWalletId: UserWalletId, + private val isRefreshing: Boolean, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy( + pullToRefreshConfig = prevState.pullToRefreshConfig.toUpdatedState(isRefreshing), + tokensListState = prevState.tokensListState.toUpdatedState(), + ) + } + is WalletState.SingleCurrency.Content -> { + prevState.copy( + pullToRefreshConfig = prevState.pullToRefreshConfig.toUpdatedState(isRefreshing), + buttons = prevState.buttons.toUpdatedState(), + ) + } + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + -> prevState + } + } + + private fun WalletPullToRefreshConfig.toUpdatedState(isRefreshing: Boolean): WalletPullToRefreshConfig { + return copy(isRefreshing = isRefreshing) + } + + private fun WalletTokensListState.toUpdatedState(): WalletTokensListState { + return if (this is WalletTokensListState.ContentState.Content && organizeTokensButtonConfig != null) { + copy( + organizeTokensButtonConfig = organizeTokensButtonConfig.copy( + isEnabled = !isRefreshing, + ), + ) + } else { + this + } + } + + private fun PersistentList.toUpdatedState(): PersistentList { + val isButtonsEnabled = !isRefreshing + + return mutate { + it.mapNotNull { button -> + when (button) { + is WalletManageButton.Buy -> button.copy(enabled = isButtonsEnabled) + is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled) + is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled) + is WalletManageButton.Receive -> button + is WalletManageButton.Swap -> null + } + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListErrorTransformer.kt new file mode 100644 index 0000000000..4586c30ab6 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListErrorTransformer.kt @@ -0,0 +1,38 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState +import timber.log.Timber + +internal class SetTokenListErrorTransformer( + userWalletId: UserWalletId, + private val error: TokenListError, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (error) { + is TokenListError.EmptyTokens -> { + when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy(tokensListState = WalletTokensListState.Empty) + } + is WalletState.MultiCurrency.Locked, + -> { + Timber.e("Impossible to load tokens list for locked wallet") + prevState + } + is WalletState.SingleCurrency, + -> { + Timber.e("Impossible to load tokens list for single-currency wallet") + prevState + } + } + } + is TokenListError.DataError, + is TokenListError.UnableToSortTokenList, + -> prevState + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListTransformer.kt new file mode 100644 index 0000000000..f982dff23f --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListTransformer.kt @@ -0,0 +1,69 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state2.ManageTokensButtonConfig +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.state2.transformers.converter.MultiWalletCardStateConverter +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.TokenListStateConverter +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import timber.log.Timber + +internal class SetTokenListTransformer( + private val tokenList: TokenList, + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val clickIntents: WalletClickIntentsV2, +) : WalletStateTransformer(userWallet.walletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy( + walletCardState = prevState.walletCardState.toLoadedState(), + tokensListState = prevState.tokensListState.toLoadedState(), + manageTokensButtonConfig = createManageTokensButtonConfig(), + ) + } + is WalletState.MultiCurrency.Locked, + -> { + Timber.e("Impossible to load tokens list for locked wallet") + prevState + } + is WalletState.SingleCurrency, + -> { + Timber.e("Impossible to load tokens list for single-currency wallet") + prevState + } + } + } + + private fun WalletCardState.toLoadedState(): WalletCardState { + return MultiWalletCardStateConverter( + fiatBalance = tokenList.totalFiatBalance, + selectedWallet = userWallet, + appCurrency = appCurrency, + ).convert(value = this) + } + + private fun WalletTokensListState.toLoadedState(): WalletTokensListState { + return TokenListStateConverter( + tokenList = tokenList, + selectedWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + ).convert(value = this) + } + + private fun createManageTokensButtonConfig(): ManageTokensButtonConfig? { + return if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + null + } else { + ManageTokensButtonConfig(clickIntents::onManageTokensClick) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountErrorTransformer.kt new file mode 100644 index 0000000000..26f55b70a4 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountErrorTransformer.kt @@ -0,0 +1,68 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryStateError +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.TxHistoryItemStateConverter +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.toImmutableList +import timber.log.Timber + +internal class SetTxHistoryCountErrorTransformer( + private val userWallet: UserWallet, + private val error: TxHistoryStateError, + private val pendingTransactions: Set, + private val clickIntents: WalletClickIntentsV2, +) : WalletStateTransformer(userWallet.walletId) { + + private val txHistoryItemConverter by lazy { + val blockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() + TxHistoryItemStateConverter( + symbol = blockchain.currency, + decimals = blockchain.decimals(), + clickIntents = clickIntents, + ) + } + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> prevState.toErrorState() + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to load transactions history for locked wallet") + prevState + } + is WalletState.MultiCurrency, + -> { + Timber.e("Impossible to load transactions history for multi-currency wallet") + prevState + } + } + } + + private fun WalletState.SingleCurrency.Content.toErrorState(): WalletState { + return copy( + txHistoryState = when (error) { + is TxHistoryStateError.EmptyTxHistories -> { + TxHistoryState.Empty(onExploreClick = clickIntents::onExploreClick) + } + is TxHistoryStateError.DataError -> { + TxHistoryState.Error( + onReloadClick = clickIntents::onReloadClick, + onExploreClick = clickIntents::onExploreClick, + ) + } + is TxHistoryStateError.TxHistoryNotImplemented -> { + TxHistoryState.NotSupported( + pendingTransactions = txHistoryItemConverter.convertList(pendingTransactions) + .toImmutableList(), + onExploreClick = clickIntents::onExploreClick, + ) + } + }, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountTransformer.kt new file mode 100644 index 0000000000..082d5aa124 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountTransformer.kt @@ -0,0 +1,60 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import androidx.paging.PagingData +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import timber.log.Timber + +internal class SetTxHistoryCountTransformer( + userWalletId: UserWalletId, + private val transactionsCount: Int, + private val clickIntents: WalletClickIntentsV2, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> prevState.toLoadingState() + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to load transactions history for locked wallet") + prevState + } + is WalletState.MultiCurrency, + -> { + Timber.e("Impossible to load transactions history for multi-currency wallet") + prevState + } + } + } + + private fun WalletState.SingleCurrency.Content.toLoadingState(): WalletState { + return if (txHistoryState is TxHistoryState.Content) { + (txHistoryState as? TxHistoryState.Content)?.contentItems?.update { + Timber.d("Load transactions history: $transactionsCount") + PagingData.from(data = createLoadingItems()) + } + this + } else { + val txHistoryContent = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = PagingData.from(data = createLoadingItems()), + ), + ) + copy(txHistoryState = txHistoryContent) + } + } + + private fun createLoadingItems(): List { + return buildList { + add(TxHistoryState.TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) + (1..transactionsCount).forEach { + add(TxHistoryState.TxHistoryItemState.Transaction(state = TransactionState.Loading(it.toString()))) + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsErrorTransformer.kt new file mode 100644 index 0000000000..37e26fa534 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsErrorTransformer.kt @@ -0,0 +1,41 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.txhistory.models.TxHistoryListError +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import timber.log.Timber + +internal class SetTxHistoryItemsErrorTransformer( + userWalletId: UserWalletId, + private val error: TxHistoryListError, + private val clickIntents: WalletClickIntentsV2, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> { + prevState.copy( + txHistoryState = when (error) { + is TxHistoryListError.DataError -> { + TxHistoryState.Error( + onReloadClick = clickIntents::onReloadClick, + onExploreClick = clickIntents::onExploreClick, + ) + } + }, + ) + } + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to load transactions history for locked wallet") + prevState + } + is WalletState.MultiCurrency -> { + Timber.e("Impossible to load transactions history for multi-currency wallet") + prevState + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsTransformer.kt new file mode 100644 index 0000000000..1bd03fe9b1 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsTransformer.kt @@ -0,0 +1,41 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import androidx.paging.PagingData +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.TxHistoryItemFlowConverter +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.coroutines.flow.Flow +import timber.log.Timber + +internal class SetTxHistoryItemsTransformer( + private val userWallet: UserWallet, + private val flow: Flow>, + private val clickIntents: WalletClickIntentsV2, +) : WalletStateTransformer(userWallet.walletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> { + val converter = TxHistoryItemFlowConverter( + userWallet = userWallet, + currentState = prevState, + clickIntents = clickIntents, + ) + prevState.copy( + txHistoryState = converter.convert(value = flow), + ) + } + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to load transactions history for locked wallet") + prevState + } + is WalletState.MultiCurrency -> { + Timber.e("Impossible to load transactions history for multi-currency wallet") + prevState + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetWarningsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetWarningsTransformer.kt new file mode 100644 index 0000000000..064c72d1e7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetWarningsTransformer.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import kotlinx.collections.immutable.ImmutableList +import timber.log.Timber + +internal class SetWarningsTransformer( + userWalletId: UserWalletId, + private val warnings: ImmutableList, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> prevState.copy(warnings = warnings) + is WalletState.SingleCurrency.Content -> prevState.copy(warnings = warnings) + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to update notifications for locked wallet") + prevState + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UnlockWalletTransformer.kt new file mode 100644 index 0000000000..bd5c0bd61b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UnlockWalletTransformer.kt @@ -0,0 +1,53 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWallet +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.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletLoadingStateFactory +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.toImmutableList +import timber.log.Timber + +internal class UnlockWalletTransformer( + private val unlockedWallets: List, + private val clickIntents: WalletClickIntentsV2, +) : WalletScreenStateTransformer { + + private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory(clickIntents = clickIntents) } + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + topBarConfig = prevState.topBarConfig.toUnlockedState(), + wallets = prevState.wallets + .map { state -> + val unlockedWallet = getUnlockedWallet(state.walletCardState.id) + if (unlockedWallet == null) state else createLoadingState(state, unlockedWallet) + } + .toImmutableList(), + ) + } + + private fun WalletTopBarConfig.toUnlockedState(): WalletTopBarConfig { + return copy(onDetailsClick = clickIntents::onDetailsClick) + } + + private fun getUnlockedWallet(walletId: UserWalletId): UserWallet? { + return unlockedWallets.firstOrNull { it.walletId == walletId } + } + + private fun createLoadingState(prevState: WalletState, unlockedWallet: UserWallet): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + -> walletLoadingStateFactory.create(userWallet = unlockedWallet) + is WalletState.MultiCurrency.Content, + is WalletState.SingleCurrency.Content, + -> { + Timber.e("Impossible to unlock wallet with content state") + prevState + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateBalanceHidingModeTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateBalanceHidingModeTransformer.kt new file mode 100644 index 0000000000..6dd7da1e64 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateBalanceHidingModeTransformer.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState + +internal class UpdateBalanceHidingModeTransformer( + private val isHidingMode: Boolean, +) : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy(isHidingMode = isHidingMode) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateWalletCardsCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateWalletCardsCountTransformer.kt new file mode 100644 index 0000000000..fe89479e52 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateWalletCardsCountTransformer.kt @@ -0,0 +1,42 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import timber.log.Timber + +internal class UpdateWalletCardsCountTransformer( + private val userWallet: UserWallet, +) : WalletStateTransformer(userWallet.walletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy(walletCardState = prevState.walletCardState.toUpdatedState()) + } + is WalletState.SingleCurrency.Content -> { + prevState.copy(walletCardState = prevState.walletCardState.toUpdatedState()) + } + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to update wallet cards count for locked wallet") + prevState + } + } + } + + private fun WalletCardState.toUpdatedState(): WalletCardState { + return when (this) { + is WalletCardState.Content -> copy( + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = userWallet), + imageResId = WalletImageResolver.resolve(userWallet = userWallet), + cardCount = userWallet.getCardsCount(), + ) + else -> this + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletStateTransformer.kt new file mode 100644 index 0000000000..38af9d7f93 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletStateTransformer.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import kotlinx.collections.immutable.toImmutableList + +internal abstract class WalletStateTransformer( + protected val userWalletId: UserWalletId, +) : WalletScreenStateTransformer { + + abstract fun transform(prevState: WalletState): WalletState + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + wallets = prevState.wallets + .map { state -> + if (state.walletCardState.id == userWalletId) transform(state) else state + } + .toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCardStateConverter.kt new file mode 100644 index 0000000000..8c3b84bf05 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCardStateConverter.kt @@ -0,0 +1,64 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.utils.converter.Converter + +internal class MultiWalletCardStateConverter( + private val fiatBalance: TokenList.FiatBalance, + private val selectedWallet: UserWallet, + private val appCurrency: AppCurrency, +) : Converter { + + override fun convert(value: WalletCardState): WalletCardState { + return when (fiatBalance) { + is TokenList.FiatBalance.Loading -> value.toLoadingState() + is TokenList.FiatBalance.Failed -> value.toErrorState() + is TokenList.FiatBalance.Loaded -> value.toWalletCardState(fiatBalance) + } + } + + private fun WalletCardState.toLoadingState(): WalletCardState { + return WalletCardState.Loading( + id = id, + title = title, + additionalInfo = additionalInfo, + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + ) + } + + private fun WalletCardState.toErrorState(): WalletCardState { + return WalletCardState.Error( + id = id, + title = title, + additionalInfo = additionalInfo, + imageResId = imageResId, + onDeleteClick = onDeleteClick, + onRenameClick = onRenameClick, + ) + } + + private fun WalletCardState.toWalletCardState(fiatBalance: TokenList.FiatBalance.Loaded): WalletCardState { + return WalletCardState.Content( + id = id, + title = title, + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = selectedWallet), + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + balance = BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatBalance.amount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + cardCount = selectedWallet.getCardsCount(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCurrencyActionsConverter.kt new file mode 100644 index 0000000000..270fa05c9c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -0,0 +1,96 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletCurrencyActionsClickIntentsImplementor +import com.tangem.utils.converter.Converter +import com.tangem.utils.isNullOrZero +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal class MultiWalletCurrencyActionsConverter( + private val userWallet: UserWallet, + private val clickIntents: WalletCurrencyActionsClickIntentsImplementor, +) : Converter> { + + override fun convert(value: TokenActionsState): ImmutableList { + return value.states + .filterIfSingleWithToken() + .mapNotNull { + mapTokenActionState(actionsState = it, cryptoCurrencyStatus = value.cryptoCurrencyStatus) + } + .toImmutableList() + } + + private fun List.filterIfSingleWithToken(): List { + return if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + filter { it !is TokenActionsState.ActionState.HideToken } + } else { + this + } + } + + private fun mapTokenActionState( + actionsState: TokenActionsState.ActionState, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): TokenActionButtonConfig? { + if (actionsState is TokenActionsState.ActionState.Send && cryptoCurrencyStatus.value.amount.isNullOrZero()) { + return null + } + + val title: TextReference + val icon: Int + val action: () -> Unit + when (actionsState) { + is TokenActionsState.ActionState.Buy -> { + title = resourceReference(R.string.common_buy) + icon = R.drawable.ic_plus_24 + action = { clickIntents.onBuyClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.Receive -> { + title = resourceReference(R.string.common_receive) + icon = R.drawable.ic_arrow_down_24 + action = { clickIntents.onReceiveClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.Sell -> { + title = resourceReference(R.string.common_sell) + icon = R.drawable.ic_currency_24 + action = { clickIntents.onSellClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.Send -> { + title = resourceReference(R.string.common_send) + icon = R.drawable.ic_arrow_up_24 + action = { clickIntents.onSendClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.Swap -> { + title = resourceReference(R.string.common_swap) + icon = R.drawable.ic_exchange_horizontal_24 + action = { clickIntents.onSwapClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.CopyAddress -> { + title = resourceReference(R.string.common_copy_address) + icon = R.drawable.ic_copy_24 + action = { clickIntents.onCopyAddressClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.HideToken -> { + title = resourceReference(R.string.token_details_hide_token) + icon = R.drawable.ic_hide_24 + action = { clickIntents.onHideTokensClick(cryptoCurrencyStatus) } + } + } + + return TokenActionButtonConfig( + text = title, + iconResId = icon, + onClick = action, + isWarning = actionsState is TokenActionsState.ActionState.HideToken, + enabled = actionsState.enabled, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletCardStateConverter.kt new file mode 100644 index 0000000000..e54cf09dfa --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletCardStateConverter.kt @@ -0,0 +1,78 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.utils.converter.Converter + +internal class SingleWalletCardStateConverter( + private val status: CryptoCurrencyStatus.Status, + private val selectedWallet: UserWallet, + private val appCurrency: AppCurrency, +) : Converter { + + override fun convert(value: WalletCardState): WalletCardState { + return when (status) { + is CryptoCurrencyStatus.Loading -> value.toLoadingState() + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Unreachable, + -> value.toErrorState() + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.NoAccount, + is CryptoCurrencyStatus.NoAmount, + -> value.toContentState(status) + } + } + + private fun WalletCardState.toLoadingState(): WalletCardState { + return WalletCardState.Loading( + id = id, + title = title, + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + ) + } + + private fun WalletCardState.toErrorState(): WalletCardState { + return WalletCardState.Error( + id = id, + title = title, + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + ) + } + + private fun WalletCardState.toContentState(status: CryptoCurrencyStatus.Status): WalletCardState { + return WalletCardState.Content( + id = id, + title = title, + additionalInfo = WalletAdditionalInfoFactory.resolve( + wallet = selectedWallet, + currencyAmount = status.amount, + ), + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + balance = formatFiatAmount(status = status, appCurrency = appCurrency), + cardCount = selectedWallet.getCardsCount(), + ) + } + + private fun formatFiatAmount(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String { + val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatAmount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletMarketPriceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletMarketPriceConverter.kt new file mode 100644 index 0000000000..1f1237f976 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletMarketPriceConverter.kt @@ -0,0 +1,68 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class SingleWalletMarketPriceConverter( + private val status: CryptoCurrencyStatus.Status, + private val appCurrency: AppCurrency, +) : Converter { + + override fun convert(value: MarketPriceBlockState): MarketPriceBlockState { + return when (status) { + CryptoCurrencyStatus.Loading -> MarketPriceBlockState.Loading(value.currencySymbol) + is CryptoCurrencyStatus.NoAccount -> value.toNoAccountState() + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.NoAmount, + -> value.toContentState() + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.Unreachable, + -> MarketPriceBlockState.Error(value.currencySymbol) + } + } + + private fun MarketPriceBlockState.toNoAccountState(): MarketPriceBlockState { + return if (status.fiatRate == null) MarketPriceBlockState.Error(currencySymbol) else toContentState() + } + + private fun MarketPriceBlockState.toContentState(): MarketPriceBlockState { + return MarketPriceBlockState.Content( + currencySymbol = currencySymbol, + price = formatPrice(status = status, appCurrency = appCurrency), + priceChangeConfig = PriceChangeState.Content( + valueInPercent = formatPriceChange(status = status), + type = getPriceChangeType(status = status), + ), + ) + } + + private fun formatPrice(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String { + val fiatRate = status.fiatRate ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatRate, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + + private fun formatPriceChange(status: CryptoCurrencyStatus.Status): String { + val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + + return BigDecimalFormatter.formatPercent(percent = priceChange, useAbsoluteValue = true) + } + + private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeType { + val priceChange = status.priceChange ?: return PriceChangeType.DOWN + + return if (priceChange > BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenItemStateConverter.kt new file mode 100644 index 0000000000..eef6aea5c9 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenItemStateConverter.kt @@ -0,0 +1,122 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.common.Provider +import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class TokenItemStateConverter( + private val appCurrencyProvider: Provider, + private val clickIntents: WalletClickIntentsV2, +) : Converter { + + private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) + + override fun convert(value: CryptoCurrencyStatus): TokenItemState { + return when (value.value) { + is CryptoCurrencyStatus.Loading -> value.mapToLoadingState() + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> value.mapToTokenItemState() + is CryptoCurrencyStatus.MissedDerivation -> value.mapToNoAddressTokenItemState() + is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, + -> value.mapToUnreachableTokenItemState() + } + } + + private fun CryptoCurrencyStatus.mapToLoadingState(): TokenItemState.Loading { + return TokenItemState.Loading( + id = currency.id.value, + iconState = iconStateConverter.convert(value = this), + titleState = TokenItemState.TitleState.Content(text = currency.name), + ) + } + + private fun CryptoCurrencyStatus.mapToTokenItemState(): TokenItemState.Content { + return TokenItemState.Content( + id = currency.id.value, + iconState = iconStateConverter.convert(value = this), + titleState = TokenItemState.TitleState.Content( + text = currency.name, + hasPending = value.hasCurrentNetworkTransactions, + ), + fiatAmountState = TokenItemState.FiatAmountState.Content( + text = getFormattedFiatAmount(), + ), + cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = getFormattedAmount()), + cryptoPriceState = getCryptoPriceState(), + onItemClick = { clickIntents.onTokenItemClick(currency) }, + onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, + ) + } + + private fun CryptoCurrencyStatus.getFormattedAmount(): String { + val amount = value.amount ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN + + return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) + } + + private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { + val fiatAmount = value.fiatAmount ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN + val appCurrency = appCurrencyProvider() + + return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) + } + + private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable( + id = currency.id.value, + iconState = iconStateConverter.convert(value = this), + titleState = TokenItemState.TitleState.Content(text = currency.name), + onItemClick = { clickIntents.onTokenItemClick(currency) }, + onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, + ) + + private fun CryptoCurrencyStatus.mapToNoAddressTokenItemState() = TokenItemState.NoAddress( + id = currency.id.value, + iconState = iconStateConverter.convert(this), + titleState = TokenItemState.TitleState.Content(text = currency.name), + onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, + ) + + private fun CryptoCurrencyStatus.getCryptoPriceState(): TokenItemState.CryptoPriceState { + val fiatRate = value.fiatRate + val priceChange = value.priceChange + + return if (fiatRate != null && priceChange != null) { + TokenItemState.CryptoPriceState.Content( + price = fiatRate.getFormattedCryptoPrice(), + priceChangePercent = BigDecimalFormatter.formatPercent( + percent = priceChange, + useAbsoluteValue = true, + maxFractionDigits = 1, + minFractionDigits = 1, + ), + type = priceChange.getPriceChangeType(), + ) + } else { + TokenItemState.CryptoPriceState.Unknown + } + } + + private fun BigDecimal.getFormattedCryptoPrice(): String { + val appCurrency = appCurrencyProvider() + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = this, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + + private fun BigDecimal.getPriceChangeType(): PriceChangeType { + return if (this > BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenListStateConverter.kt new file mode 100644 index 0000000000..b9b2ab5cf3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenListStateConverter.kt @@ -0,0 +1,93 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.common.Provider +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState.TokensListItemState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.persistentListOf +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState.OrganizeTokensButtonConfig as WalletOrganizeTokensButtonConfig + +internal class TokenListStateConverter( + private val tokenList: TokenList, + private val selectedWallet: UserWallet, + private val appCurrency: AppCurrency, + private val clickIntents: WalletClickIntentsV2, +) : Converter { + + private val tokenStatusConverter = TokenItemStateConverter( + appCurrencyProvider = Provider { appCurrency }, + clickIntents = clickIntents, + ) + + override fun convert(value: WalletTokensListState): WalletTokensListState { + return when (tokenList) { + is TokenList.Empty -> WalletTokensListState.Empty + is TokenList.GroupedByNetwork -> WalletTokensListState.ContentState.Content( + items = tokenList.toGroupedItems(), + organizeTokensButtonConfig = getOrganizeTokensButtonState( + currenciesSize = tokenList.groups.flatMap(NetworkGroup::currencies).size, + ), + ) + is TokenList.Ungrouped -> WalletTokensListState.ContentState.Content( + items = tokenList.toUngroupedItems(), + organizeTokensButtonConfig = getOrganizeTokensButtonState(currenciesSize = tokenList.currencies.size), + ) + } + } + + private fun TokenList.GroupedByNetwork.toGroupedItems(): PersistentList { + return groups.fold(initial = persistentListOf()) { acc, group -> + acc.mutate { it.addGroup(group) } + } + } + + private fun TokenList.Ungrouped.toUngroupedItems(): PersistentList { + return currencies.fold(initial = persistentListOf()) { acc, token -> + acc.mutate { it.addToken(token) } + } + } + + private fun MutableList.addGroup(group: NetworkGroup): List { + val groupTitle = TokensListItemState.NetworkGroupTitle( + id = group.network.hashCode(), + name = stringReference(group.network.name), + ) + + add(groupTitle) + group.currencies.forEach { token -> addToken(token) } + + return this + } + + private fun MutableList.addToken(token: CryptoCurrencyStatus): List { + val tokenItemState = tokenStatusConverter.convert(token) + add(TokensListItemState.Token(tokenItemState)) + + return this + } + + private fun getOrganizeTokensButtonState(currenciesSize: Int): WalletOrganizeTokensButtonConfig? { + return if (currenciesSize > 1 && !isSingleCurrencyWalletWithToken()) { + WalletOrganizeTokensButtonConfig( + isEnabled = tokenList.totalFiatBalance !is TokenList.FiatBalance.Loading, + onClick = clickIntents::onOrganizeTokensClick, + ) + } else { + null + } + } + + private fun isSingleCurrencyWalletWithToken(): Boolean { + return selectedWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt new file mode 100644 index 0000000000..d8209a170d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt @@ -0,0 +1,144 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import android.text.format.DateUtils +import androidx.paging.* +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isToday +import com.tangem.utils.extensions.isYesterday +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import java.util.UUID + +private val scope = CoroutineScope(Dispatchers.IO) + +internal class TxHistoryItemFlowConverter( + private val userWallet: UserWallet, + private val currentState: WalletState.SingleCurrency.Content, + private val clickIntents: WalletClickIntentsV2, +) : Converter>, TxHistoryState?> { + + private val txHistoryItemConverter by lazy { + val blockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() + TxHistoryItemStateConverter( + symbol = blockchain.currency, + decimals = blockchain.decimals(), + clickIntents = clickIntents, + ) + } + + override fun convert(value: Flow>): TxHistoryState { + val txHistoryContent = currentState.txHistoryState as? TxHistoryState.Content + ?: TxHistoryState.Content(contentItems = MutableStateFlow(PagingData.empty())) + + // FIXME: TxHistoryRepository should send loading transactions + // [REDACTED_JIRA] + value + .onEach { txHistoryStatePagingData -> + txHistoryContent.contentItems.update { + txHistoryStatePagingData + .map { item -> + // [createTransactionState] returns timestamp without formatting + TxHistoryItemState.Transaction(state = createTransactionState(item)) + } + .insertHeaderItem( + terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE, + item = TxHistoryItemState.Title(clickIntents::onExploreClick), + ) + .insertGroupTitle() // method uses the raw timestamp + .formatTransactionsTimestamp() // method formats the timestamp + } + } + .cachedIn(scope) + .launchIn(scope) + + return txHistoryContent + } + + private fun createTransactionState(item: TxHistoryItem): TransactionState { + return txHistoryItemConverter.convert(value = item) + } + + private fun PagingData.insertGroupTitle(): PagingData { + return insertSeparators(terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE) { before, after -> + // Use raw timestamp to get date + + // If [afterDate] is the first transaction in the flow, add the group title + val afterDate = after.getTimestamp()?.toDateFormat() ?: return@insertSeparators null + if (before is TxHistoryItemState.Title) { + return@insertSeparators TxHistoryItemState.GroupTitle(afterDate, itemKey = UUID.randomUUID().toString()) + } + + /* + * If [beforeDate] is not equals to [afterDate], then [afterDate] is first transaction in + * the new group + */ + val beforeDate = before.getTimestamp()?.toDateFormat() ?: return@insertSeparators null + return@insertSeparators if (beforeDate != afterDate) { + TxHistoryItemState.GroupTitle(afterDate, itemKey = UUID.randomUUID().toString()) + } else { + null + } + } + } + + /** + * Map the [PagingData] to format the [TxHistoryItemState] timestamp + */ + private fun PagingData.formatTransactionsTimestamp(): PagingData { + return map { txHistoryItemState -> + if (txHistoryItemState is TxHistoryItemState.Transaction && + txHistoryItemState.state is TransactionState.Content + ) { + val txContent = txHistoryItemState.state as TransactionState.Content + txHistoryItemState.copy( + state = txContent.copy(timestamp = txContent.timestamp.toTimeFormat()), + ) + } else { + txHistoryItemState + } + } + } + + private fun TxHistoryItemState?.getTimestamp(): Long? { + return if (this is TxHistoryItemState.Transaction && this.state is TransactionState.Content) { + val txContent = this.state as TransactionState.Content + requireNotNull(txContent.timestamp.toLongOrNull()) { "Timestamp must be Long type" } + } else { + null + } + } + + /** + * If [this] timestamp is today or yesterday, returns relative date, + * otherwise returns formatting date. + */ + private fun Long.toDateFormat(): String { + val localDate = DateTime(this, DateTimeZone.getDefault()) + return if (localDate.isToday() || localDate.isYesterday()) { + DateUtils.getRelativeTimeSpanString( + this, + DateTime.now().millis, + DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE, + ).toString() + } else { + DateTimeFormatters.formatDate(date = localDate) + } + } + + private fun String.toTimeFormat(): String { + return DateTimeFormatters.formatTime(time = DateTime(this.toLong(), DateTimeZone.getDefault())) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemStateConverter.kt new file mode 100644 index 0000000000..e9c78803f3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemStateConverter.kt @@ -0,0 +1,109 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import com.tangem.utils.converter.Converter +import com.tangem.utils.toBriefAddressFormat +import com.tangem.utils.toFormattedCurrencyString + +internal class TxHistoryItemStateConverter( + private val symbol: String, + private val decimals: Int, + private val clickIntents: WalletClickIntentsV2, +) : Converter { + + override fun convert(value: TxHistoryItem): TransactionState { + return createTransactionStateItem(item = value) + } + + @Suppress("LongMethod") + private fun createTransactionStateItem(item: TxHistoryItem): TransactionState { + return TransactionState.Content( + txHash = item.txHash, + amount = item.getAmount(), + timestamp = item.getRawTimestamp(), + status = item.status.tiUiStatus(), + direction = item.extractDirection(), + iconRes = item.extractIcon(), + title = item.extractTitle(), + subtitle = item.extractSubtitle(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, + ) + } + + private fun TxHistoryItem.extractIcon(): Int = if (status == TxHistoryItem.TransactionStatus.Failed) { + R.drawable.ic_close_24 + } else { + when (type) { + is TxHistoryItem.TransactionType.Approve -> R.drawable.ic_doc_24 + is TxHistoryItem.TransactionType.Operation, + is TxHistoryItem.TransactionType.Swap, + is TxHistoryItem.TransactionType.Transfer, + is TxHistoryItem.TransactionType.UnknownOperation, + -> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24 + } + } + + private fun TxHistoryItem.extractTitle(): TextReference = when (val type = type) { + is TxHistoryItem.TransactionType.Approve -> resourceReference(R.string.common_approval) + is TxHistoryItem.TransactionType.Operation -> stringReference(type.name) + is TxHistoryItem.TransactionType.Swap -> resourceReference(R.string.common_swap) + is TxHistoryItem.TransactionType.Transfer -> resourceReference(R.string.common_transfer) + is TxHistoryItem.TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation) + } + + private fun TxHistoryItem.extractSubtitle(): TextReference = + when (val interactionAddress = interactionAddressType) { + is TxHistoryItem.InteractionAddressType.Contract -> resourceReference( + id = R.string.transaction_history_contract_address, + formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()), + ) + is TxHistoryItem.InteractionAddressType.Multiple -> resourceReference( + id = if (isOutgoing) { + R.string.transaction_history_transaction_to_address + } else { + R.string.transaction_history_transaction_from_address + }, + formatArgs = wrappedList(resourceReference(R.string.transaction_history_multiple_addresses)), + ) + is TxHistoryItem.InteractionAddressType.User -> resourceReference( + id = if (isOutgoing) { + R.string.transaction_history_transaction_to_address + } else { + R.string.transaction_history_transaction_from_address + }, + formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()), + ) + } + + private fun TxHistoryItem.extractDirection() = + if (isOutgoing) TransactionState.Content.Direction.OUTGOING else TransactionState.Content.Direction.INCOMING + + /** + * Get timestamp without formatting. + * It's life hack that help us to add transaction's group title to flow. + * + * @see [convert] + */ + private fun TxHistoryItem.getRawTimestamp() = this.timestampInMillis.toString() + + private fun TxHistoryItem.TransactionStatus.tiUiStatus() = when (this) { + TxHistoryItem.TransactionStatus.Confirmed -> TransactionState.Content.Status.Confirmed + TxHistoryItem.TransactionStatus.Failed -> TransactionState.Content.Status.Failed + TxHistoryItem.TransactionStatus.Unconfirmed -> TransactionState.Content.Status.Unconfirmed + } + + private fun TxHistoryItem.getAmount(): String { + val prefix = when (status) { + TxHistoryItem.TransactionStatus.Failed -> "" + else -> if (isOutgoing) "-" else "+" + } + return prefix + amount.toFormattedCurrencyString(currency = symbol, decimals = decimals) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/UserWalletConverterExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/UserWalletConverterExt.kt new file mode 100644 index 0000000000..75d10c7b80 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/UserWalletConverterExt.kt @@ -0,0 +1,16 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.utils + +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState + +internal inline fun UserWallet.createStateByWalletType( + multiCurrencyCreator: () -> WalletState.MultiCurrency, + singleCurrencyCreator: () -> WalletState.SingleCurrency, +): WalletState { + return if (isWalletWithTokens()) multiCurrencyCreator() else singleCurrencyCreator() +} + +private fun UserWallet.isWalletWithTokens(): Boolean { + return isMultiCurrency || scanResponse.cardTypesResolver.isSingleWalletWithToken() +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletEventSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletEventSender.kt new file mode 100644 index 0000000000..6cc3953821 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletEventSender.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.utils + +import com.tangem.core.ui.event.consumedEvent +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SendEventTransformer +import javax.inject.Inject + +/** + * Component for sending events [WalletEvent] on WalletScreen + * + * @property stateHolder state holder for changing state + * +[REDACTED_AUTHOR] + */ +internal class WalletEventSender @Inject constructor( + private val stateHolder: WalletStateHolderV2, +) { + + fun send(event: WalletEvent) { + stateHolder.update(transformer = SendEventTransformer(event = event, onConsume = ::onConsume)) + } + + private fun onConsume() { + stateHolder.update { + it.copy(event = consumedEvent()) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletLoadingStateFactory.kt new file mode 100644 index 0000000000..055d23befa --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletLoadingStateFactory.kt @@ -0,0 +1,85 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.utils + +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig +import com.tangem.feature.wallet.presentation.wallet.state2.ManageTokensButtonConfig +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.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow + +/** + * Factory for creating loading state [WalletState] + * + * @property clickIntents click intents + */ +internal class WalletLoadingStateFactory(private val clickIntents: WalletClickIntentsV2) { + + fun create(userWallet: UserWallet): WalletState { + return userWallet.createStateByWalletType( + multiCurrencyCreator = { createLoadingMultiCurrencyContent(userWallet) }, + singleCurrencyCreator = { createLoadingSingleCurrencyContent(userWallet) }, + ) + } + + private fun createLoadingMultiCurrencyContent(userWallet: UserWallet): WalletState.MultiCurrency.Content { + return WalletState.MultiCurrency.Content( + pullToRefreshConfig = createPullToRefreshConfig(), + walletCardState = userWallet.toLoadingWalletCardState(), + warnings = persistentListOf(), + bottomSheetConfig = null, + tokensListState = WalletTokensListState.ContentState.Loading, + manageTokensButtonConfig = ManageTokensButtonConfig(clickIntents::onManageTokensClick), + ) + } + + private fun createLoadingSingleCurrencyContent(userWallet: UserWallet): WalletState.SingleCurrency.Content { + val currencySymbol = userWallet.scanResponse.cardTypesResolver.getBlockchain().currency + return WalletState.SingleCurrency.Content( + pullToRefreshConfig = createPullToRefreshConfig(), + walletCardState = userWallet.toLoadingWalletCardState(), + warnings = persistentListOf(), + bottomSheetConfig = null, + buttons = createDisabledButtons(), + marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = currencySymbol), + txHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), + ), + ), + ) + } + + private fun createPullToRefreshConfig(): WalletPullToRefreshConfig { + return WalletPullToRefreshConfig(onRefresh = clickIntents::onRefreshSwipe, isRefreshing = false) + } + + private fun UserWallet.toLoadingWalletCardState(): WalletCardState { + return WalletCardState.Loading( + id = walletId, + title = name, + additionalInfo = if (isMultiCurrency) WalletAdditionalInfoFactory.resolve(wallet = this) else null, + imageResId = WalletImageResolver.resolve(userWallet = this), + onRenameClick = clickIntents::onRenameClick, + onDeleteClick = clickIntents::onDeleteBeforeConfirmationClick, + ) + } + + private fun createDisabledButtons(): PersistentList { + return persistentListOf( + WalletManageButton.Buy(enabled = false, onClick = {}), + WalletManageButton.Send(enabled = false, onClick = {}), + WalletManageButton.Receive(enabled = false, onClick = {}), + WalletManageButton.Sell(enabled = false, onClick = {}), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt new file mode 100644 index 0000000000..29df476122 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.domain.wallets.models.UserWalletId +import javax.inject.Inject + +/** +[REDACTED_AUTHOR] + */ +internal interface WalletCardClickIntents { + + fun onRenameClick(userWalletId: UserWalletId, name: String) + + fun onDeleteBeforeConfirmationClick(userWalletId: UserWalletId) + + fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) +} + +internal class WalletCardClickIntentsImplementor @Inject constructor() : WalletCardClickIntents { + + override fun onRenameClick(userWalletId: UserWalletId, name: String) { + // TODO + } + + override fun onDeleteBeforeConfirmationClick(userWalletId: UserWalletId) { + // TODO + } + + override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) { + // TODO + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt new file mode 100644 index 0000000000..4903e65c4d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt @@ -0,0 +1,27 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import javax.inject.Inject + +internal class WalletClickIntentsV2 @Inject constructor( + private val walletCardClickIntentsImplementor: WalletCardClickIntentsImplementor, + private val warningsClickIntentsImplementer: WalletWarningsClickIntentsImplementer, + private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor, + private val contentClickIntentsImplementor: WalletContentClickIntentsImplementor, +) : WalletCardClickIntents by walletCardClickIntentsImplementor, + WalletWarningsClickIntents by warningsClickIntentsImplementer, + WalletCurrencyActionsClickIntents by currencyActionsClickIntentsImplementor, + WalletContentClickIntents by contentClickIntentsImplementor { + + @Suppress("UnusedPrivateMember") + fun onWalletChange(index: Int) { + // TODO + } + + fun onRefreshSwipe() { + // TODO + } + + fun onReloadClick() { + // TODO + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt new file mode 100644 index 0000000000..02fc91d677 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt @@ -0,0 +1,53 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import javax.inject.Inject + +internal interface WalletContentClickIntents { + + fun onBackClick() + + fun onDetailsClick() + + fun onManageTokensClick() + + fun onOrganizeTokensClick() + + fun onTokenItemClick(currency: CryptoCurrency) + + fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onTransactionClick(txHash: String) +} + +internal class WalletContentClickIntentsImplementor @Inject constructor() : WalletContentClickIntents { + + override fun onBackClick() { + // TODO + } + + override fun onDetailsClick() { + // TODO + } + + override fun onManageTokensClick() { + // TODO + } + + override fun onOrganizeTokensClick() { + // TODO + } + + override fun onTokenItemClick(currency: CryptoCurrency) { + // TODO + } + + override fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO + } + + override fun onTransactionClick(txHash: String) { + // TODO + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt new file mode 100644 index 0000000000..dd10ad4337 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -0,0 +1,64 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import javax.inject.Inject + +interface WalletCurrencyActionsClickIntents { + + fun onSendClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onCopyAddressClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onHideTokensClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onPerformHideToken(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onBuyClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onSwapClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onExploreClick() +} + +internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor() : WalletCurrencyActionsClickIntents { + + override fun onSendClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO + } + + override fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO + } + + override fun onCopyAddressClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO + } + + override fun onHideTokensClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO + } + + override fun onPerformHideToken(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO + } + + override fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO + } + + override fun onBuyClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO + } + + override fun onSwapClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO + } + + override fun onExploreClick() { + // TODO + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt new file mode 100644 index 0000000000..99dfe53ffb --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -0,0 +1,64 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.domain.tokens.model.CryptoCurrency +import javax.inject.Inject + +internal interface WalletWarningsClickIntents { + + fun onAddBackupCardClick() + + fun onCloseAlreadySignedHashesWarningClick() + + fun onGenerateMissedAddressesClick(missedAddressCurrencies: List) + + fun onOpenUnlockWalletsBottomSheetClick() + + fun onUnlockWalletClick() + + fun onScanToUnlockWalletClick() + + fun onLikeAppClick() + + fun onDislikeAppClick() + + fun onCloseRateAppWarningClick() +} + +internal class WalletWarningsClickIntentsImplementer @Inject constructor() : WalletWarningsClickIntents { + + override fun onAddBackupCardClick() { + // TODO + } + + override fun onCloseAlreadySignedHashesWarningClick() { + // TODO + } + + override fun onGenerateMissedAddressesClick(missedAddressCurrencies: List) { + // TODO + } + + override fun onOpenUnlockWalletsBottomSheetClick() { + // TODO + } + + override fun onUnlockWalletClick() { + // TODO + } + + override fun onScanToUnlockWalletClick() { + // TODO + } + + override fun onLikeAppClick() { + // TODO + } + + override fun onDislikeAppClick() { + // TODO + } + + override fun onCloseRateAppWarningClick() { + // TODO + } +} \ No newline at end of file From aabfc1bba15cb78d473b808be2ef31d4f42dc7fe Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 20:04:12 +0800 Subject: [PATCH 06/20] Updated on 2026-08-14 --- .../converter/TxHistoryItemFlowConverter.kt | 32 ++----------------- 1 file changed, 3 insertions(+), 29 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt index d8209a170d..6aeb577d67 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt @@ -1,24 +1,20 @@ package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter -import android.text.format.DateUtils import androidx.paging.* import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState -import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toDateFormat +import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state2.WalletState import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 import com.tangem.utils.converter.Converter -import com.tangem.utils.extensions.isToday -import com.tangem.utils.extensions.isYesterday import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* -import org.joda.time.DateTime -import org.joda.time.DateTimeZone import java.util.UUID private val scope = CoroutineScope(Dispatchers.IO) @@ -103,7 +99,7 @@ internal class TxHistoryItemFlowConverter( ) { val txContent = txHistoryItemState.state as TransactionState.Content txHistoryItemState.copy( - state = txContent.copy(timestamp = txContent.timestamp.toTimeFormat()), + state = txContent.copy(timestamp = txContent.timestamp.toLong().toTimeFormat()), ) } else { txHistoryItemState @@ -119,26 +115,4 @@ internal class TxHistoryItemFlowConverter( null } } - - /** - * If [this] timestamp is today or yesterday, returns relative date, - * otherwise returns formatting date. - */ - private fun Long.toDateFormat(): String { - val localDate = DateTime(this, DateTimeZone.getDefault()) - return if (localDate.isToday() || localDate.isYesterday()) { - DateUtils.getRelativeTimeSpanString( - this, - DateTime.now().millis, - DateUtils.DAY_IN_MILLIS, - DateUtils.FORMAT_ABBREV_RELATIVE, - ).toString() - } else { - DateTimeFormatters.formatDate(date = localDate) - } - } - - private fun String.toTimeFormat(): String { - return DateTimeFormatters.formatTime(time = DateTime(this.toLong(), DateTimeZone.getDefault())) - } } \ No newline at end of file From 422afbc17f24d62fbe51cfdf1a4868ed149cd4ef Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 14:58:11 +0800 Subject: [PATCH 07/20] Updated on 2026-08-14 --- .../presentation/wallet/domain/UseCaseExt.kt | 60 +++ .../intents/BaseWalletClickIntents.kt | 26 ++ .../intents/WalletCardClickIntents.kt | 54 ++- .../intents/WalletClickIntentsV2.kt | 133 ++++++- .../intents/WalletContentClickIntents.kt | 92 ++++- .../WalletCurrencyActionsClickIntents.kt | 372 +++++++++++++++++- .../intents/WalletWarningsClickIntents.kt | 241 +++++++++++- 7 files changed, 934 insertions(+), 44 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/BaseWalletClickIntents.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt new file mode 100644 index 0000000000..7e62eb79e3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt @@ -0,0 +1,60 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import kotlinx.coroutines.flow.* +import timber.log.Timber + +internal fun GetSelectedWalletSyncUseCase.unwrap(): UserWallet? { + return this().fold( + ifLeft = { + Timber.e("Impossible to get selected wallet $it") + null + }, + ifRight = { it }, + ) +} + +internal suspend fun GetPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId: UserWalletId): CryptoCurrencyStatus? { + return this(userWalletId) + .conflate() + .distinctUntilChanged() + .filter(Either::isRight) + .firstOrNull() + ?.fold( + ifLeft = { + Timber.e("Impossible to get primary currency status $it") + null + }, + ifRight = { it }, + ) +} + +internal suspend fun GetSelectedAppCurrencyUseCase.unwrap(): AppCurrency { + return this() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .firstOrNull() + ?: AppCurrency.Default +} + +internal suspend fun GetPrimaryCurrencyStatusUpdatesUseCase.collectLatest( + userWalletId: UserWalletId, + onRight: suspend (CryptoCurrencyStatus) -> Unit, +) { + this(userWalletId = userWalletId) + .conflate() + .distinctUntilChanged() + .collectLatest { maybeStatus -> + maybeStatus.onRight { onRight(it) } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/BaseWalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/BaseWalletClickIntents.kt new file mode 100644 index 0000000000..d9be6984f5 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/BaseWalletClickIntents.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import kotlinx.coroutines.CoroutineScope +import kotlin.properties.Delegates + +/** + * Base wallet click intents component. + * Provides router and viewModelScope to child classes. + * +[REDACTED_AUTHOR] + */ +@Suppress("UnnecessaryAbstractClass") +internal abstract class BaseWalletClickIntents { + + protected val router: InnerWalletRouter get() = _router + protected val viewModelScope: CoroutineScope get() = _viewModelScope + + private var _router: InnerWalletRouter by Delegates.notNull() + private var _viewModelScope: CoroutineScope by Delegates.notNull() + + open fun initialize(router: InnerWalletRouter, coroutineScope: CoroutineScope) { + _router = router + _viewModelScope = coroutineScope + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt index 29df476122..e7cc3d17a9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt @@ -1,11 +1,20 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents +import com.tangem.core.navigation.AppScreen import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.DeleteWalletUseCase +import com.tangem.domain.wallets.usecase.UpdateWalletUseCase +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.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject -/** -[REDACTED_AUTHOR] - */ internal interface WalletCardClickIntents { fun onRenameClick(userWalletId: UserWalletId, name: String) @@ -15,17 +24,48 @@ internal interface WalletCardClickIntents { fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) } -internal class WalletCardClickIntentsImplementor @Inject constructor() : WalletCardClickIntents { +internal class WalletCardClickIntentsImplementor @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val walletEventSender: WalletEventSender, + // TODO: private val walletScreenContentLoader: WalletScreenContentLoader, + private val updateWalletUseCase: UpdateWalletUseCase, + private val deleteWalletUseCase: DeleteWalletUseCase, + private val dispatchers: CoroutineDispatcherProvider, +) : BaseWalletClickIntents(), WalletCardClickIntents { override fun onRenameClick(userWalletId: UserWalletId, name: String) { - // TODO + viewModelScope.launch(dispatchers.main) { + updateWalletUseCase(userWalletId = userWalletId, update = { it.copy(name) }) + } } override fun onDeleteBeforeConfirmationClick(userWalletId: UserWalletId) { - // TODO + walletEventSender.send( + event = WalletEvent.ShowAlert( + state = WalletAlertState.RemoveWalletAlert( + onConfirmClick = { onDeleteAfterConfirmationClick(userWalletId) }, + ), + ), + ) } override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) { - // TODO + viewModelScope.launch(dispatchers.main) { + // TODO: walletScreenContentLoader.cancel(userWalletId) + deleteWalletUseCase(userWalletId) + .onRight { popBackIfAllWalletsIsLocked() } + .onLeft { Timber.e(it.toString()) } + } + } + + private fun popBackIfAllWalletsIsLocked() { + val wallets = stateHolder.value.wallets.map(WalletState::walletCardState) + val unlockedWallet = wallets.count { it !is WalletCardState.LockedContent } + + if (unlockedWallet == 1) { + router.popBackStack( + screen = if (wallets.size > 1) AppScreen.Welcome else AppScreen.Home, + ) + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt index 4903e65c4d..643c5dec03 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt @@ -1,27 +1,150 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.settings.NeverToShowWalletsScrollPreview +import com.tangem.domain.tokens.FetchCardTokenListUseCase +import com.tangem.domain.tokens.FetchCurrencyStatusUseCase +import com.tangem.domain.tokens.FetchTokenListUseCase +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.usecase.SelectWalletUseCase +import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import com.tangem.feature.wallet.presentation.wallet.analytics.PortfolioEvent +import com.tangem.feature.wallet.presentation.wallet.domain.unwrap +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetRefreshStateTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTokenListErrorTransformer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import javax.inject.Inject +/** +[REDACTED_AUTHOR] + */ +@Suppress("LongParameterList") internal class WalletClickIntentsV2 @Inject constructor( private val walletCardClickIntentsImplementor: WalletCardClickIntentsImplementor, private val warningsClickIntentsImplementer: WalletWarningsClickIntentsImplementer, private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor, private val contentClickIntentsImplementor: WalletContentClickIntentsImplementor, -) : WalletCardClickIntents by walletCardClickIntentsImplementor, + private val stateHolder: WalletStateHolderV2, + // TODO: private val walletScreenContentLoader: WalletScreenContentLoader, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val selectWalletUseCase: SelectWalletUseCase, + // TODO: private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val fetchTokenListUseCase: FetchTokenListUseCase, + private val fetchCardTokenListUseCase: FetchCardTokenListUseCase, + private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, + private val neverToShowWalletsScrollPreview: NeverToShowWalletsScrollPreview, + private val analyticsEventHandler: AnalyticsEventHandler, + private val dispatchers: CoroutineDispatcherProvider, +) : BaseWalletClickIntents(), + WalletCardClickIntents by walletCardClickIntentsImplementor, WalletWarningsClickIntents by warningsClickIntentsImplementer, WalletCurrencyActionsClickIntents by currencyActionsClickIntentsImplementor, WalletContentClickIntents by contentClickIntentsImplementor { - @Suppress("UnusedPrivateMember") + override fun initialize(router: InnerWalletRouter, coroutineScope: CoroutineScope) { + super.initialize(router, coroutineScope) + + walletCardClickIntentsImplementor.initialize(router, coroutineScope) + warningsClickIntentsImplementer.initialize(router, coroutineScope) + currencyActionsClickIntentsImplementor.initialize(router, coroutineScope) + contentClickIntentsImplementor.initialize(router, coroutineScope) + } + fun onWalletChange(index: Int) { - // TODO + viewModelScope.launch(dispatchers.main) { + launch(dispatchers.main) { neverToShowWalletsScrollPreview() } + + val maybeUserWallet = selectWalletUseCase( + userWalletId = stateHolder.value.wallets[index].walletCardState.id, + ) + + stateHolder.update { it.copy(selectedWalletIndex = index) } + + maybeUserWallet.onRight { + // TODO: + // walletScreenContentLoader.load( + // userWallet = it, + // appCurrency = getSelectedAppCurrencyUseCase.unwrap(), + // clickIntents = this@WalletClickIntentsV2, + // coroutineScope = viewModelScope, + // ) + } + } } fun onRefreshSwipe() { - // TODO + when (stateHolder.getSelectedWallet()) { + is WalletState.MultiCurrency.Content -> { + analyticsEventHandler.send(PortfolioEvent.Refreshed) + refreshMultiCurrencyContent() + } + is WalletState.SingleCurrency.Content -> { + analyticsEventHandler.send(PortfolioEvent.Refreshed) + refreshSingleCurrencyContent() + } + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + -> Unit + } + } + + private fun refreshMultiCurrencyContent() { + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + stateHolder.update( + SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = true), + ) + + viewModelScope.launch(dispatchers.main) { + val maybeFetchResult = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + fetchCardTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) + } else { + fetchTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) + } + + maybeFetchResult.onLeft { + stateHolder.update(SetTokenListErrorTransformer(userWalletId = userWallet.walletId, error = it)) + } + + stateHolder.update( + SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = false), + ) + } } fun onReloadClick() { - // TODO + refreshSingleCurrencyContent() + } + + // FIXME: refreshSingleCurrencyContent mustn't update the TxHistory and Buttons. It only must fetch primary + // currency. Now it not works because GetPrimaryCurrency's subscriber uses .distinctUntilChanged() + private fun refreshSingleCurrencyContent() { + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + stateHolder.update( + SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = true), + ) + + viewModelScope.launch(dispatchers.main) { + fetchCurrencyStatusUseCase(userWallet.walletId, refresh = true) + + // TODO: + // walletScreenContentLoader.load( + // userWallet = userWallet, + // appCurrency = getSelectedAppCurrencyUseCase.unwrap(), + // clickIntents = this@WalletClickIntentsV2, + // coroutineScope = viewModelScope, + // isRefresh = true, + // ) + + stateHolder.update( + SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = false), + ) + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt index 02fc91d677..82c943e41b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt @@ -1,7 +1,27 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.TokensAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.feature.wallet.presentation.wallet.analytics.PortfolioEvent +import com.tangem.feature.wallet.presentation.wallet.domain.unwrap +import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.CloseBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.OpenBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.MultiWalletCurrencyActionsConverter +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch import javax.inject.Inject internal interface WalletContentClickIntents { @@ -21,33 +41,81 @@ internal interface WalletContentClickIntents { fun onTransactionClick(txHash: String) } -internal class WalletContentClickIntentsImplementor @Inject constructor() : WalletContentClickIntents { +@Suppress("LongParameterList") +internal class WalletContentClickIntentsImplementor @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, + private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, + private val dispatchers: CoroutineDispatcherProvider, + private val reduxStateHolder: ReduxStateHolder, +) : BaseWalletClickIntents(), WalletContentClickIntents { - override fun onBackClick() { - // TODO - } + override fun onBackClick() = router.popBackStack() - override fun onDetailsClick() { - // TODO - } + override fun onDetailsClick() = router.openDetailsScreen() override fun onManageTokensClick() { - // TODO + analyticsEventHandler.send(PortfolioEvent.ButtonManageTokens) + reduxStateHolder.dispatch(action = TokensAction.SetArgs.ManageAccess) + router.openManageTokensScreen() } override fun onOrganizeTokensClick() { - // TODO + analyticsEventHandler.send(PortfolioEvent.OrganizeTokens) + router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId()) } override fun onTokenItemClick(currency: CryptoCurrency) { - // TODO + analyticsEventHandler.send(PortfolioEvent.TokenTapped) + router.openTokenDetails(stateHolder.getSelectedWalletId(), currency) } override fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + viewModelScope.launch(dispatchers.main) { + getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = cryptoCurrencyStatus) + .take(count = 1) + .collectLatest { + showActionsBottomSheet(it, userWallet) + } + } + } + + private fun showActionsBottomSheet(tokenActionsState: TokenActionsState, userWallet: UserWallet) { + stateHolder.update( + OpenBottomSheetTransformer( + userWalletId = userWallet.walletId, + content = ActionsBottomSheetConfig( + actions = MultiWalletCurrencyActionsConverter( + userWallet = userWallet, + clickIntents = currencyActionsClickIntentsImplementor, + ).convert(tokenActionsState), + ), + onDismissBottomSheet = { + stateHolder.update( + CloseBottomSheetTransformer(userWalletId = userWallet.walletId), + ) + }, + ), + ) } override fun onTransactionClick(txHash: String) { - // TODO + viewModelScope.launch(dispatchers.main) { + val currency = getPrimaryCurrencyStatusUpdatesUseCase.unwrap( + userWalletId = stateHolder.getSelectedWalletId(), + ) + ?.currency + ?: return@launch + + router.openUrl( + url = getExplorerTransactionUrlUseCase(txHash = txHash, networkId = currency.network.id), + ) + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index dd10ad4337..0b6070971a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -1,6 +1,46 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents +import com.tangem.blockchain.common.address.Address +import com.tangem.blockchain.common.address.AddressType +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.extensions.WrappedList +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase +import com.tangem.domain.tokens.RemoveCurrencyUseCase +import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent +import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent +import com.tangem.domain.walletconnect.WalletConnectActions +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.domain.unwrap +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.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.CloseBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.OpenBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch import javax.inject.Inject interface WalletCurrencyActionsClickIntents { @@ -24,41 +64,353 @@ interface WalletCurrencyActionsClickIntents { fun onExploreClick() } -internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor() : WalletCurrencyActionsClickIntents { +@Suppress("LongParameterList", "LargeClass") +internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val walletEventSender: WalletEventSender, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val walletManagersFacade: WalletManagersFacade, + private val isDemoCardUseCase: IsDemoCardUseCase, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, + private val removeCurrencyUseCase: RemoveCurrencyUseCase, + private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, + private val getExploreUrlUseCase: GetExploreUrlUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, + private val dispatchers: CoroutineDispatcherProvider, + private val reduxStateHolder: ReduxStateHolder, +) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { override fun onSendClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonSend(cryptoCurrencyStatus.currency.symbol), + ) + + stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWallet.walletId)) + + when (val currency = cryptoCurrencyStatus.currency) { + is CryptoCurrency.Coin -> sendCoin(cryptoCurrencyStatus, userWallet) + is CryptoCurrency.Token -> sendToken(currency, cryptoCurrencyStatus.value, userWallet) + } + } + + private fun sendCoin(cryptoCurrencyStatus: CryptoCurrencyStatus, userWallet: UserWallet) { + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.SendCoin(userWallet = userWallet, coinStatus = cryptoCurrencyStatus), + ) + } + + private fun sendToken( + cryptoCurrency: CryptoCurrency.Token, + cryptoCurrencyStatus: CryptoCurrencyStatus.Status, + userWallet: UserWallet, + ) { + viewModelScope.launch(dispatchers.main) { + getNetworkCoinStatusUseCase( + userWalletId = userWallet.walletId, + networkId = cryptoCurrency.network.id, + derivationPath = cryptoCurrency.network.derivationPath, + isSingleWalletWithTokens = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), + ) + .take(count = 1) + .collectLatest { + it.onRight { coinStatus -> + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.SendToken( + userWallet = userWallet, + tokenCurrency = cryptoCurrency, + tokenFiatRate = cryptoCurrencyStatus.fiatRate, + coinFiatRate = coinStatus.value.fiatRate, + ), + ) + } + } + } } override fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO + val userWalletId = stateHolder.getSelectedWalletId() + + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrencyStatus.currency.symbol), + ) + + viewModelScope.launch(dispatchers.main) { + val currency = cryptoCurrencyStatus.currency + val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network) + + analyticsEventHandler.send(event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened) + + stateHolder.update( + OpenBottomSheetTransformer( + userWalletId = userWalletId, + content = createReceiveBottomSheetContent(currency, addresses), + onDismissBottomSheet = { + stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) + }, + ), + ) + } + } + + private fun createReceiveBottomSheetContent( + currency: CryptoCurrency, + addresses: List
, + ): TangemBottomSheetConfigContent { + return TokenReceiveBottomSheetConfig( + name = currency.name, + symbol = currency.symbol, + network = currency.network.name, + addresses = addresses.map { address -> + AddressModel( + value = address.value, + type = AddressModel.Type.valueOf(address.type.name), + ) + }, + onCopyClick = { + analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) + }, + onShareClick = { + analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) + }, + ) } override fun onCopyAddressClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonCopyAddress(cryptoCurrencyStatus.currency.symbol), + ) + + viewModelScope.launch(dispatchers.main) { + walletManagersFacade.getAddress( + userWalletId = stateHolder.getSelectedWalletId(), + network = cryptoCurrencyStatus.currency.network, + ) + .find { it.type == AddressType.Default } + ?.value + ?.let { + walletEventSender.send( + event = WalletEvent.CopyAddress( + address = it, + toast = resourceReference(R.string.wallet_notification_address_copied), + ), + ) + } + } } override fun onHideTokensClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonRemoveToken(cryptoCurrencyStatus.currency.symbol), + ) + + viewModelScope.launch(dispatchers.main) { + walletEventSender.send( + event = WalletEvent.ShowAlert( + state = getHideTokeAlertConfig(stateHolder.getSelectedWalletId(), cryptoCurrencyStatus), + ), + ) + } + } + + private suspend fun getHideTokeAlertConfig( + userWalletId: UserWalletId, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): WalletAlertState.DefaultAlert { + val currency = cryptoCurrencyStatus.currency + return if (currency is CryptoCurrency.Coin && !isCryptoCurrencyCoinCouldHide(userWalletId, currency)) { + WalletAlertState.DefaultAlert( + title = resourceReference( + id = R.string.token_details_unable_hide_alert_title, + formatArgs = WrappedList(listOf(cryptoCurrencyStatus.currency.name)), + ), + message = resourceReference( + id = R.string.token_details_unable_hide_alert_message, + formatArgs = WrappedList( + listOf( + cryptoCurrencyStatus.currency.name, + cryptoCurrencyStatus.currency.network.name, + ), + ), + ), + onConfirmClick = null, + ) + } else { + WalletAlertState.DefaultAlert( + title = resourceReference( + id = R.string.token_details_hide_alert_title, + formatArgs = WrappedList(listOf(cryptoCurrencyStatus.currency.name)), + ), + message = resourceReference(R.string.token_details_hide_alert_message), + onConfirmClick = { onPerformHideToken(cryptoCurrencyStatus) }, + ) + } } override fun onPerformHideToken(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO + val userWalletId = stateHolder.getSelectedWalletId() + + viewModelScope.launch(dispatchers.io) { + removeCurrencyUseCase(userWalletId, cryptoCurrencyStatus.currency) + .fold( + ifLeft = { + walletEventSender.send( + event = WalletEvent.ShowToast(text = resourceReference(R.string.common_error)), + ) + }, + ifRight = { + getSelectedWalletSyncUseCase.unwrap()?.let { userWallet -> + reduxStateHolder.dispatch( + action = WalletConnectActions.New.SetupUserChains(userWallet = userWallet), + ) + } + + stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) + }, + ) + } } override fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonSell(cryptoCurrencyStatus.currency.symbol), + ) + + showErrorIfDemoModeOrElse { + viewModelScope.launch(dispatchers.main) { + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.Sell( + cryptoCurrencyStatus = cryptoCurrencyStatus, + appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code, + ), + ) + } + } } override fun onBuyClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonBuy(cryptoCurrencyStatus.currency.symbol), + ) + + showErrorIfDemoModeOrElse { + viewModelScope.launch(dispatchers.main) { + reduxStateHolder.dispatch( + TradeCryptoAction.New.Buy( + userWallet = userWallet, + cryptoCurrencyStatus = cryptoCurrencyStatus, + appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code, + ), + ) + } + } } override fun onSwapClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonExchange(cryptoCurrencyStatus.currency.symbol), + ) + + reduxStateHolder.dispatch(TradeCryptoAction.New.Swap(cryptoCurrencyStatus.currency)) } override fun onExploreClick() { - // TODO + showErrorIfDemoModeOrElse(action = ::openExplorer) + } + + private fun openExplorer() { + val userWalletId = stateHolder.getSelectedWalletId() + + viewModelScope.launch(dispatchers.main) { + val currency = getPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId)?.currency ?: return@launch + val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network) + + if (addresses.size == 1) { + router.openUrl( + url = getExploreUrlUseCase( + userWalletId = userWalletId, + currency = currency, + addressType = AddressType.Default, + ), + ) + } else { + showChooseAddressBottomSheet(userWalletId, addresses, currency) + } + } + } + + private fun showChooseAddressBottomSheet( + userWalletId: UserWalletId, + addresses: List
, + currency: CryptoCurrency, + ) { + stateHolder.update( + OpenBottomSheetTransformer( + userWalletId = userWalletId, + content = ChooseAddressBottomSheetConfig( + addressModels = addresses + .map { address -> + AddressModel( + value = address.value, + type = AddressModel.Type.valueOf(address.type.name), + ) + } + .toImmutableList(), + onClick = { + onAddressTypeSelected( + userWalletId = userWalletId, + currency = currency, + addressModel = it, + ) + }, + ), + onDismissBottomSheet = { + stateHolder.update( + CloseBottomSheetTransformer(userWalletId = userWalletId), + ) + }, + ), + ) + } + + private fun onAddressTypeSelected( + userWalletId: UserWalletId, + currency: CryptoCurrency, + addressModel: AddressModel, + ) { + viewModelScope.launch(dispatchers.main) { + router.openUrl( + url = getExploreUrlUseCase( + userWalletId = userWalletId, + currency = currency, + addressType = AddressType.valueOf(addressModel.type.name), + ), + ) + + stateHolder.update( + CloseBottomSheetTransformer(userWalletId = userWalletId), + ) + } + } + + private fun showErrorIfDemoModeOrElse(action: () -> Unit) { + val cardId = getSelectedWalletSyncUseCase.unwrap()?.cardId ?: return + + if (isDemoCardUseCase(cardId = cardId)) { + stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId())) + + walletEventSender.send( + event = WalletEvent.ShowError( + text = resourceReference(id = R.string.alert_demo_feature_disabled), + ), + ) + } else { + action() + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index 99dfe53ffb..b4b9b2d06f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -1,6 +1,44 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents +import com.tangem.blockchain.blockchains.cardano.CardanoUtils +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.extensions.toMapKey +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.card.DerivePublicKeysUseCase +import com.tangem.domain.card.SetCardWasScannedUseCase +import com.tangem.domain.common.configs.CardConfig +import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.LegacyAction +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.settings.NeverToSuggestRateAppUseCase +import com.tangem.domain.settings.RemindToRateAppLaterUseCase +import com.tangem.domain.tokens.FetchTokenListUseCase import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UnlockWalletsError +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase +import com.tangem.domain.wallets.usecase.UpdateWalletUseCase +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen +import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletClickHandler +import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletError +import com.tangem.feature.wallet.presentation.wallet.domain.unwrap +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.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.CloseBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.OpenBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender +import com.tangem.operations.derivation.ExtendedPublicKeysMap +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.launch import javax.inject.Inject internal interface WalletWarningsClickIntents { @@ -24,41 +62,224 @@ internal interface WalletWarningsClickIntents { fun onCloseRateAppWarningClick() } -internal class WalletWarningsClickIntentsImplementer @Inject constructor() : WalletWarningsClickIntents { +@Suppress("LongParameterList") +internal class WalletWarningsClickIntentsImplementer @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val walletEventSender: WalletEventSender, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val updateWalletUseCase: UpdateWalletUseCase, + private val unlockWalletsUseCase: UnlockWalletsUseCase, + private val derivePublicKeysUseCase: DerivePublicKeysUseCase, + private val scanCardToUnlockWalletClickHandler: ScanCardToUnlockWalletClickHandler, + private val fetchTokenListUseCase: FetchTokenListUseCase, + private val setCardWasScannedUseCase: SetCardWasScannedUseCase, + private val neverToSuggestRateAppUseCase: NeverToSuggestRateAppUseCase, + private val remindToRateAppLaterUseCase: RemindToRateAppLaterUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, + private val reduxStateHolder: ReduxStateHolder, + private val dispatchers: CoroutineDispatcherProvider, +) : BaseWalletClickIntents(), WalletWarningsClickIntents { override fun onAddBackupCardClick() { - // TODO + analyticsEventHandler.send(MainScreen.NoticeBackupYourWalletTapped) + router.openOnboardingScreen() } override fun onCloseAlreadySignedHashesWarningClick() { - // TODO + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + viewModelScope.launch(dispatchers.main) { + setCardWasScannedUseCase(cardId = userWallet.cardId) + } } override fun onGenerateMissedAddressesClick(missedAddressCurrencies: List) { - // TODO + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + analyticsEventHandler.send(Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Main)) + analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped) + + viewModelScope.launch(dispatchers.main) { + deriveMissingCurrencies( + scanResponse = userWallet.scanResponse, + currencyList = missedAddressCurrencies, + ) { scannedCardResponse -> + updateWalletUseCase( + userWalletId = userWallet.walletId, + update = { it.copy(scanResponse = scannedCardResponse) }, + ) + .onRight { fetchTokenListUseCase(userWalletId = it.walletId) } + } + } } + // TODO: [REDACTED_JIRA] + private fun deriveMissingCurrencies( + scanResponse: ScanResponse, + currencyList: List, + onSuccess: suspend (ScanResponse) -> Unit, + ) { + val config = CardConfig.createConfig(scanResponse.card) + val derivationDataList = currencyList.mapNotNull { + config.primaryCurve(blockchain = Blockchain.fromId(it.network.id.value))?.let { curve -> + getNewDerivations(curve, scanResponse, it) + } + } + + val derivations = buildMap> { + derivationDataList.forEach { + val current = this[it.derivations.first] + if (current != null) { + current.addAll(it.derivations.second) + current.distinct() + } else { + this[it.derivations.first] = it.derivations.second.toMutableList() + } + } + }.ifEmpty { return } + + viewModelScope.launch(dispatchers.io) { + derivePublicKeysUseCase(cardId = null, derivations = derivations) + .onRight { + val newDerivedKeys = it.entries + val oldDerivedKeys = scanResponse.derivedKeys + + val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet() + + val updatedDerivedKeys = walletKeys.associateWith { walletKey -> + val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap()) + val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap()) + ExtendedPublicKeysMap(oldDerivations + newDerivations) + } + val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys) + + onSuccess(updatedScanResponse) + } + } + } + + private fun getNewDerivations( + curve: EllipticCurve, + scanResponse: ScanResponse, + currency: CryptoCurrency, + ): DerivationData? { + val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null + + val blockchain = Blockchain.fromId(currency.network.id.value) + val supportedCurves = blockchain.getSupportedCurves() + val path = blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) + .takeIf { supportedCurves.contains(curve) } + + val customPath = currency.network.derivationPath.value?.let { + DerivationPath(it) + }.takeIf { supportedCurves.contains(curve) } + + val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList() + if (bothCandidates.isEmpty()) return null + + if (currency is CryptoCurrency.Coin && blockchain == Blockchain.Cardano) { + currency.network.derivationPath.value?.let { + bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it))) + } + } + + val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() + val alreadyDerivedKeys: ExtendedPublicKeysMap = + scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap()) + val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList() + + val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) } + if (toDerive.isEmpty()) return null + + return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) + } + + class DerivationData(val derivations: Pair>) + override fun onOpenUnlockWalletsBottomSheetClick() { - // TODO + stateHolder.update( + OpenBottomSheetTransformer( + content = requireNotNull(stateHolder.getSelectedWallet().bottomSheetConfig).content, + userWalletId = stateHolder.getSelectedWalletId(), + onDismissBottomSheet = { + stateHolder.update( + CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId()), + ) + }, + ), + ) } override fun onUnlockWalletClick() { - // TODO + analyticsEventHandler.send(MainScreen.NoticeWalletLocked) + + viewModelScope.launch(dispatchers.main) { + unlockWalletsUseCase(throwIfNotAllWalletsUnlocked = true) + .onRight { stateHolder.update(CloseBottomSheetTransformer(stateHolder.getSelectedWalletId())) } + .onLeft(::handleUnlockWalletsError) + } + } + + private fun handleUnlockWalletsError(error: UnlockWalletsError) { + val event = when (error) { + is UnlockWalletsError.DataError, + is UnlockWalletsError.UnableToUnlockWallets, + -> WalletEvent.ShowToast(resourceReference(R.string.user_wallet_list_error_unable_to_unlock)) + is UnlockWalletsError.NoUserWalletSelected, + is UnlockWalletsError.NotAllUserWalletsUnlocked, + -> WalletEvent.ShowAlert(WalletAlertState.RescanWallets) + } + + walletEventSender.send(event) } override fun onScanToUnlockWalletClick() { - // TODO + analyticsEventHandler.send(event = MainScreen.WalletUnlockTapped) + + viewModelScope.launch(dispatchers.main) { + scanCardToUnlockWalletClickHandler(walletId = stateHolder.getSelectedWalletId()) + .onLeft { error -> + when (error) { + ScanCardToUnlockWalletError.WrongCardIsScanned -> { + walletEventSender.send( + event = WalletEvent.ShowAlert(WalletAlertState.WrongCardIsScanned), + ) + } + ScanCardToUnlockWalletError.ManyScanFails -> router.openScanFailedDialog() + } + } + } } override fun onLikeAppClick() { - // TODO + analyticsEventHandler.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Liked)) + + walletEventSender.send( + event = WalletEvent.RateApp( + onDismissClick = { + viewModelScope.launch(dispatchers.main) { + neverToSuggestRateAppUseCase() + } + }, + ), + ) } override fun onDislikeAppClick() { - // TODO + analyticsEventHandler.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Disliked)) + + viewModelScope.launch(dispatchers.main) { + neverToSuggestRateAppUseCase() + + reduxStateHolder.dispatch(LegacyAction.SendEmailRateCanBeBetter) + } } override fun onCloseRateAppWarningClick() { - // TODO + analyticsEventHandler.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Closed)) + + viewModelScope.launch(dispatchers.main) { + remindToRateAppLaterUseCase() + } } } \ No newline at end of file From 5384cf75ad15f558c2e7119c8caea2d9f604ad0c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 15:13:54 +0800 Subject: [PATCH 08/20] Updated on 2026-08-14 --- .../utils/TokenListAnalyticsSender.kt | 62 ++++++ .../domain/GetMultiWalletWarningsFactory.kt | 203 ++++++++++++++++++ .../domain/GetSingleWalletWarningsFactory.kt | 184 ++++++++++++++++ .../wallet/domain/WalletWithFundsChecker.kt | 38 ++++ .../MultiWalletWarningsSubscriber.kt | 37 ++++ .../subscribers/PrimaryCurrencySubscriber.kt | 92 ++++++++ .../SingleWalletButtonsSubscriber.kt | 45 ++++ .../SingleWalletNotificationsSubscriber.kt | 40 ++++ .../SingleWalletWithTokenListSubscriber.kt | 60 ++++++ .../wallet/subscribers/TokenListSubscriber.kt | 62 ++++++ .../wallet/subscribers/TxHistorySubscriber.kt | 108 ++++++++++ .../wallet/subscribers/WalletSubscriber.kt | 30 +++ 12 files changed, 961 insertions(+) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListSubscriber.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt new file mode 100644 index 0000000000..eb62eaf6e3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -0,0 +1,62 @@ +package com.tangem.feature.wallet.presentation.wallet.analytics.utils + +import arrow.core.Either +import com.tangem.common.extensions.isZero +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent +import dagger.hilt.android.scopes.ViewModelScoped +import java.math.BigDecimal +import javax.inject.Inject + +@ViewModelScoped +internal class TokenListAnalyticsSender @Inject constructor( + private val analyticsEventHandler: AnalyticsEventHandler, +) { + + fun send(maybeTokenList: Either) { + val tokenList = (maybeTokenList as? Either.Right)?.value ?: return + + createCardBalanceState(tokenList)?.let { + analyticsEventHandler.send(event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded(balance = it)) + } + } + + private fun createCardBalanceState(tokenList: TokenList): AnalyticsParam.CardBalanceState? { + return when (val fiatBalance = tokenList.totalFiatBalance) { + is TokenList.FiatBalance.Failed -> fiatBalance.toCardBalanceState(tokenList) + is TokenList.FiatBalance.Loaded -> fiatBalance.toCardBalanceState() + TokenList.FiatBalance.Loading -> null + } + } + + private fun TokenList.FiatBalance.Failed.toCardBalanceState(tokenList: TokenList): AnalyticsParam.CardBalanceState { + val currenciesStatuses = when (tokenList) { + is TokenList.Empty -> emptyList() + is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) + is TokenList.Ungrouped -> tokenList.currencies + } + + return when { + currenciesStatuses.isEmpty() -> AnalyticsParam.CardBalanceState.Empty + currenciesStatuses.any { it.value is CryptoCurrencyStatus.NoQuote } -> { + AnalyticsParam.CardBalanceState.NoRate + } + else -> AnalyticsParam.CardBalanceState.BlockchainError + } + } + + private fun TokenList.FiatBalance.Loaded.toCardBalanceState(): AnalyticsParam.CardBalanceState? { + return if (amount > BigDecimal.ZERO) { + AnalyticsParam.CardBalanceState.Full + } else if (amount.isZero()) { + AnalyticsParam.CardBalanceState.Empty + } else { + null + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt new file mode 100644 index 0000000000..83355647bd --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -0,0 +1,203 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import arrow.core.Either +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.settings.IsReadyToShowRateAppUseCase +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOf +import timber.log.Timber +import javax.inject.Inject + +@ViewModelScoped +internal class GetMultiWalletWarningsFactory @Inject constructor( + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val getTokenListUseCase: GetTokenListUseCase, + private val isDemoCardUseCase: IsDemoCardUseCase, + private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, + private val isNeedToBackupUseCase: IsNeedToBackupUseCase, +) { + + private var readyForRateAppNotification = false + + fun create(clickIntents: WalletClickIntentsV2): Flow> { + val userWallet = getSelectedWalletSyncUseCase().fold( + ifLeft = { + Timber.e("Failed to get selected wallet $it") + return flowOf(value = persistentListOf()) + }, + ifRight = { it }, + ) + + val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + + return combine( + flow = getTokenListUseCase(userWallet.walletId).conflate(), + flow2 = isReadyToShowRateAppUseCase().conflate(), + flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(), + // flow4 = getMissedAddressCryptoCurrenciesUseCase(userWallet.walletId).conflate(), + ) { maybeTokenList, isReadyToShowRating, isNeedToBackup -> + // maybeTokenList.onRight { Timber.e(it.toString()) } + // maybeMissedAddressCurrencies.onRight { Timber.e(it.toString()) } + readyForRateAppNotification = true + buildList { + addCriticalNotifications(cardTypesResolver) + + addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents) + + addWarningNotifications(cardTypesResolver, maybeTokenList, isNeedToBackup, clickIntents) + + addRateTheAppNotification(isReadyToShowRating, clickIntents) + }.toImmutableList() + } + } + + private fun MutableList.addCriticalNotifications(cardTypesResolver: CardTypesResolver) { + addIf( + element = WalletNotification.Critical.DevCard, + condition = !cardTypesResolver.isReleaseFirmwareType(), + ) + + addIf( + element = WalletNotification.Critical.FailedCardValidation, + condition = cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.isAttestationFailed(), + ) + + cardTypesResolver.getRemainingSignatures()?.let { remainingSignatures -> + addIf( + element = WalletNotification.Warning.LowSignatures(count = remainingSignatures), + condition = remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT, + ) + } + } + + private fun MutableList.addInformationalNotifications( + cardTypesResolver: CardTypesResolver, + maybeTokenList: Either, + clickIntents: WalletClickIntentsV2, + ) { + addIf( + element = WalletNotification.Informational.DemoCard, + condition = isDemoCardUseCase(cardId = cardTypesResolver.getCardId()), + ) + + addMissingAddressesNotification(maybeTokenList, clickIntents) + } + + private fun MutableList.addMissingAddressesNotification( + maybeTokenList: Either, + clickIntents: WalletClickIntentsV2, + ) { + val currencies = maybeTokenList.getMissingAddressCurrencies() + + addIf( + element = WalletNotification.Informational.MissingAddresses( + missingAddressesCount = currencies.count(), + onGenerateClick = { + clickIntents.onGenerateMissedAddressesClick(missedAddressCurrencies = currencies) + }, + ), + condition = currencies.isNotEmpty(), + ) + } + + private fun Either.getMissingAddressCurrencies(): List { + return fold( + ifLeft = { emptyList() }, + ifRight = { tokenList -> + val currencies = when (tokenList) { + is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) + is TokenList.Ungrouped -> tokenList.currencies + is TokenList.Empty -> emptyList() + } + + currencies + .filter { it.value is CryptoCurrencyStatus.MissedDerivation } + .map(CryptoCurrencyStatus::currency) + }, + ) + } + + private fun MutableList.addWarningNotifications( + cardTypesResolver: CardTypesResolver, + tokenList: Either, + isNeedToBackup: Boolean, + clickIntents: WalletClickIntentsV2, + ) { + addIf( + element = WalletNotification.Warning.MissingBackup( + onStartBackupClick = clickIntents::onAddBackupCardClick, + ), + condition = isNeedToBackup, + ) + + addIf( + element = WalletNotification.Warning.TestNetCard, + condition = cardTypesResolver.isTestCard(), + ) + + addIf( + element = WalletNotification.Warning.SomeNetworksUnreachable, + condition = tokenList.hasUnreachableNetworks(), + ) + } + + private fun Either.hasUnreachableNetworks(): Boolean { + return fold( + ifLeft = { false }, + ifRight = { tokenList -> + val currencies = when (tokenList) { + is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) + is TokenList.Ungrouped -> tokenList.currencies + is TokenList.Empty -> emptyList() + } + + currencies.any { it.value is CryptoCurrencyStatus.Unreachable } + }, + ) + } + + private fun MutableList.addRateTheAppNotification( + isReadyToShowRating: Boolean, + clickIntents: WalletClickIntentsV2, + ) { + addIf( + element = WalletNotification.RateApp( + onLikeClick = clickIntents::onLikeAppClick, + onDislikeClick = clickIntents::onDislikeAppClick, + onCloseClick = clickIntents::onCloseRateAppWarningClick, + ), + condition = isReadyToShowRating && readyForRateAppNotification, + ) + } + + private fun MutableList.addIf(element: WalletNotification, condition: Boolean) { + if (condition) { + add(element = element) + if (element is WalletNotification.Critical || element is WalletNotification.Warning) { + readyForRateAppNotification = false + } + } + } + + private companion object { + const val MAX_REMAINING_SIGNATURES_COUNT = 10 + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt new file mode 100644 index 0000000000..a248e07dbe --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt @@ -0,0 +1,184 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import arrow.core.Either +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.settings.IsReadyToShowRateAppUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.* +import timber.log.Timber +import javax.inject.Inject + +@ViewModelScoped +internal class GetSingleWalletWarningsFactory @Inject constructor( + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val isDemoCardUseCase: IsDemoCardUseCase, + private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, + private val isNeedToBackupUseCase: IsNeedToBackupUseCase, + private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase, +) { + + private var readyForRateAppNotification = false + + fun create(clickIntents: WalletClickIntentsV2): Flow> { + val userWallet = getSelectedWalletSyncUseCase().fold( + ifLeft = { + Timber.e("Failed to get selected wallet $it") + return flowOf(value = persistentListOf()) + }, + ifRight = { it }, + ) + + val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + + return combine( + flow = getPrimaryCurrencyStatusUpdatesUseCase(userWallet.walletId), + flow2 = isReadyToShowRateAppUseCase().conflate(), + flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(), + ) { primaryCurrencyStatus, isReadyToShowRating, isNeedToBackup -> + readyForRateAppNotification = true + buildList { + addCriticalNotifications(cardTypesResolver) + + addInformationalNotifications(cardTypesResolver) + + addWarningNotifications( + userWallet, + cardTypesResolver, + primaryCurrencyStatus, + isNeedToBackup, + clickIntents, + ) + + addRateTheAppNotification(isReadyToShowRating, clickIntents) + }.toImmutableList() + } + } + + private fun MutableList.addCriticalNotifications(cardTypesResolver: CardTypesResolver) { + addIf( + element = WalletNotification.Critical.DevCard, + condition = !cardTypesResolver.isReleaseFirmwareType(), + ) + + addIf( + element = WalletNotification.Critical.FailedCardValidation, + condition = cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.isAttestationFailed(), + ) + + cardTypesResolver.getRemainingSignatures()?.let { remainingSignatures -> + addIf( + element = WalletNotification.Warning.LowSignatures(count = remainingSignatures), + condition = remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT, + ) + } + } + + private fun MutableList.addInformationalNotifications(cardTypesResolver: CardTypesResolver) { + addIf( + element = WalletNotification.Informational.DemoCard, + condition = isDemoCardUseCase(cardId = cardTypesResolver.getCardId()), + ) + } + + private suspend fun MutableList.addWarningNotifications( + userWallet: UserWallet, + cardTypesResolver: CardTypesResolver, + maybePrimaryCurrencyStatus: Either, + isNeedToBackup: Boolean, + clickIntents: WalletClickIntentsV2, + ) { + val cryptoCurrencyStatus = maybePrimaryCurrencyStatus.fold(ifLeft = { null }, ifRight = { it }) + + addIf( + element = WalletNotification.Warning.MissingBackup( + onStartBackupClick = clickIntents::onAddBackupCardClick, + ), + condition = isNeedToBackup, + ) + + addIf( + element = WalletNotification.Warning.TestNetCard, + condition = cardTypesResolver.isTestCard(), + ) + + addIf( + element = WalletNotification.Warning.NetworksUnreachable, + condition = cryptoCurrencyStatus?.value is CryptoCurrencyStatus.Unreachable, + ) + + addNoAccountWarning(cryptoCurrencyStatus) + + addIf( + element = WalletNotification.Warning.NumberOfSignedHashesIncorrect( + onCloseClick = clickIntents::onCloseAlreadySignedHashesWarningClick, + ), + condition = hasSignedHashes(userWallet, cryptoCurrencyStatus), + ) + } + + private fun MutableList.addNoAccountWarning(cryptoCurrencyStatus: CryptoCurrencyStatus?) { + val noAccountStatus = cryptoCurrencyStatus?.value as? CryptoCurrencyStatus.NoAccount + if (noAccountStatus != null) { + add( + element = WalletNotification.Informational.NoAccount( + network = cryptoCurrencyStatus.currency.name, + amount = noAccountStatus.amountToCreateAccount.toString(), + symbol = cryptoCurrencyStatus.currency.symbol, + ), + ) + } + } + + private suspend fun hasSignedHashes( + selectedWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus?, + ): Boolean { + return cryptoCurrencyStatus?.currency?.network?.let { + hasSingleWalletSignedHashesUseCase(userWallet = selectedWallet, network = it) + .conflate() + .distinctUntilChanged() + .firstOrNull() + } ?: false + } + + private fun MutableList.addRateTheAppNotification( + isReadyToShowRating: Boolean, + clickIntents: WalletClickIntentsV2, + ) { + addIf( + element = WalletNotification.RateApp( + onLikeClick = clickIntents::onLikeAppClick, + onDislikeClick = clickIntents::onDislikeAppClick, + onCloseClick = clickIntents::onCloseRateAppWarningClick, + ), + condition = isReadyToShowRating && readyForRateAppNotification, + ) + } + + private fun MutableList.addIf(element: WalletNotification, condition: Boolean) { + if (condition) { + add(element = element) + if (element is WalletNotification.Critical || element is WalletNotification.Warning) { + readyForRateAppNotification = false + } + } + } + + private companion object { + const val MAX_REMAINING_SIGNATURES_COUNT = 10 + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt new file mode 100644 index 0000000000..3426a96ae1 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt @@ -0,0 +1,38 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import arrow.core.Either +import com.tangem.common.extensions.isZero +import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import javax.inject.Inject + +internal class WalletWithFundsChecker @Inject constructor( + private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, +) { + + suspend fun check(maybeTokenList: Either) { + val tokenList = (maybeTokenList as? Either.Right)?.value ?: return + + val hasNonZeroWallets = when (tokenList) { + is TokenList.GroupedByNetwork -> { + tokenList.groups + .flatMap(NetworkGroup::currencies) + .hasNonZeroWallets() + } + is TokenList.Ungrouped -> tokenList.currencies.hasNonZeroWallets() + is TokenList.Empty -> false + } + + if (hasNonZeroWallets) setWalletWithFundsFoundUseCase() + } + + private fun List.hasNonZeroWallets(): Boolean { + return any { + val amount = it.value.amount ?: return@any false + !amount.isZero() + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt new file mode 100644 index 0000000000..08b89be0c7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt @@ -0,0 +1,37 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetWarningsTransformer +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onEach +import kotlin.coroutines.CoroutineContext + +internal class MultiWalletWarningsSubscriber( + private val userWalletId: UserWalletId, + private val stateHolder: WalletStateHolderV2, + private val clickIntents: WalletClickIntentsV2, + private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, +) : WalletSubscriber>(name = "multi_wallet_warnings") { + + override fun create( + coroutineScope: CoroutineScope, + uiDispatcher: CoroutineContext, + ): Flow> { + return getMultiWalletWarningsFactory.create(clickIntents) + .conflate() + .distinctUntilChanged() + .onEach { + stateHolder.update( + SetWarningsTransformer(userWalletId = userWalletId, warnings = it), + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt new file mode 100644 index 0000000000..fde79c3d02 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt @@ -0,0 +1,92 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import arrow.core.Either +import com.tangem.common.extensions.isZero +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetPrimaryCurrencyTransformer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onEach +import java.math.BigDecimal +import kotlin.coroutines.CoroutineContext + +internal class PrimaryCurrencySubscriber( + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val stateHolder: WalletStateHolderV2, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, +) : WalletSubscriber>(name = "primary_currency") { + + override fun create( + coroutineScope: CoroutineScope, + uiDispatcher: CoroutineContext, + ): Flow> { + return getPrimaryCurrencyStatusUpdatesUseCase(userWallet.walletId) + .conflate() + .distinctUntilChanged() + .onEach(::updateContent) + .onEach(::sendAnalyticsEvent) + .onEach(::checkWalletWithFunds) + } + + private fun updateContent(maybeCurrencyStatus: Either) { + val status = (maybeCurrencyStatus as? Either.Right)?.value ?: return + + stateHolder.update( + SetPrimaryCurrencyTransformer( + status = status.value, + userWallet = userWallet, + appCurrency = appCurrency, + ), + ) + } + + private fun sendAnalyticsEvent(maybeCurrencyStatus: Either) { + val status = (maybeCurrencyStatus as? Either.Right)?.value ?: return + + val fiatAmount = status.value.fiatAmount + val cardBalanceState = when (status.value) { + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.NoAccount, + is CryptoCurrencyStatus.NoAmount, + -> createCardBalanceState(fiatAmount) + is CryptoCurrencyStatus.NoQuote -> AnalyticsParam.CardBalanceState.NoRate + is CryptoCurrencyStatus.Unreachable -> AnalyticsParam.CardBalanceState.BlockchainError + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Loading, + is CryptoCurrencyStatus.Custom, + -> null + } + + cardBalanceState?.let { + analyticsEventHandler.send(event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded(balance = it)) + } + } + + private fun createCardBalanceState(fiatAmount: BigDecimal?): AnalyticsParam.CardBalanceState? { + return when { + fiatAmount == null -> null + fiatAmount.isZero() -> AnalyticsParam.CardBalanceState.Empty + else -> AnalyticsParam.CardBalanceState.Full + } + } + + private suspend fun checkWalletWithFunds(maybeCurrencyStatus: Either) { + val status = (maybeCurrencyStatus as? Either.Right)?.value ?: return + + if (status.value.amount?.isZero() == false) setWalletWithFundsFoundUseCase() + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt new file mode 100644 index 0000000000..1f0de740d0 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt @@ -0,0 +1,45 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.collectLatest +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetCryptoCurrencyActionsTransformer +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* +import kotlin.coroutines.CoroutineContext + +internal class SingleWalletButtonsSubscriber( + private val userWallet: UserWallet, + private val stateHolder: WalletStateHolderV2, + private val clickIntents: WalletClickIntentsV2, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, +) : WalletSubscriber(name = "single_wallet_buttons") { + + override fun create(coroutineScope: CoroutineScope, uiDispatcher: CoroutineContext): Flow { + return channelFlow { + getPrimaryCurrencyStatusUpdatesUseCase.collectLatest(userWalletId = userWallet.walletId) { status -> + getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = status) + .conflate() + .distinctUntilChanged() + .firstOrNull() + ?.let { send(it) } + } + } + .onEach(::updateContent) + } + + private fun updateContent(tokenActionsState: TokenActionsState) { + stateHolder.update( + SetCryptoCurrencyActionsTransformer( + tokenActionsState = tokenActionsState, + userWallet = userWallet, + clickIntents = clickIntents, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt new file mode 100644 index 0000000000..64df0d025e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt @@ -0,0 +1,40 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetWarningsTransformer +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onEach +import kotlin.coroutines.CoroutineContext + +/** +[REDACTED_AUTHOR] + */ +internal class SingleWalletNotificationsSubscriber( + private val userWalletId: UserWalletId, + private val stateHolder: WalletStateHolderV2, + private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, + private val clickIntents: WalletClickIntentsV2, +) : WalletSubscriber>(name = "single_wallet_warnings") { + + override fun create( + coroutineScope: CoroutineScope, + uiDispatcher: CoroutineContext, + ): Flow> { + return getSingleWalletWarningsFactory.create(clickIntents) + .conflate() + .distinctUntilChanged() + .onEach { + stateHolder.update( + SetWarningsTransformer(userWalletId = userWalletId, warnings = it), + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt new file mode 100644 index 0000000000..a6446944c0 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt @@ -0,0 +1,60 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import arrow.core.Either +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetCardTokensListUseCase +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTokenListErrorTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTokenListTransformer +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onEach +import kotlin.coroutines.CoroutineContext + +@Suppress("LongParameterList") +internal class SingleWalletWithTokenListSubscriber( + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val stateHolder: WalletStateHolderV2, + private val clickIntents: WalletClickIntentsV2, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, + private val walletWithFundsChecker: WalletWithFundsChecker, + private val getCardTokensListUseCase: GetCardTokensListUseCase, +) : WalletSubscriber>(name = "single_wallet_with_token_list") { + + override fun create( + coroutineScope: CoroutineScope, + uiDispatcher: CoroutineContext, + ): Flow> { + return getCardTokensListUseCase(userWalletId = userWallet.walletId) + .conflate() + .distinctUntilChanged() + .onEach(::updateContent) + .onEach(tokenListAnalyticsSender::send) + .onEach(walletWithFundsChecker::check) + } + + private fun updateContent(maybeTokenList: Either) { + stateHolder.update( + maybeTokenList.fold( + ifLeft = { SetTokenListErrorTransformer(userWalletId = userWallet.walletId, error = it) }, + ifRight = { + SetTokenListTransformer( + tokenList = it, + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + ) + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListSubscriber.kt new file mode 100644 index 0000000000..4c4caf4479 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListSubscriber.kt @@ -0,0 +1,62 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import arrow.core.Either +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTokenListErrorTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTokenListTransformer +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onEach +import kotlin.coroutines.CoroutineContext + +typealias MaybeTokenListFlow = Flow> + +@Suppress("LongParameterList") +internal class TokenListSubscriber( + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val stateHolder: WalletStateHolderV2, + private val clickIntents: WalletClickIntentsV2, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, + private val walletWithFundsChecker: WalletWithFundsChecker, + private val getTokenListUseCase: GetTokenListUseCase, +) : WalletSubscriber>(name = "token_list") { + + override fun create( + coroutineScope: CoroutineScope, + uiDispatcher: CoroutineContext, + ): Flow> { + return getTokenListUseCase(userWalletId = userWallet.walletId) + .conflate() + .distinctUntilChanged() + .onEach(::updateContent) + .onEach(tokenListAnalyticsSender::send) + .onEach(walletWithFundsChecker::check) + } + + private fun updateContent(maybeTokenList: Either) { + stateHolder.update( + maybeTokenList.fold( + ifLeft = { SetTokenListErrorTransformer(userWalletId = userWallet.walletId, error = it) }, + ifRight = { + SetTokenListTransformer( + tokenList = it, + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + ) + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt new file mode 100644 index 0000000000..6da06e3029 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt @@ -0,0 +1,108 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import androidx.paging.PagingData +import androidx.paging.cachedIn +import arrow.core.Either +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryListError +import com.tangem.domain.txhistory.models.TxHistoryStateError +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.collectLatest +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryCountErrorTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryCountTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryItemsErrorTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryItemsTransformer +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlin.coroutines.CoroutineContext + +typealias MaybeTxHistoryCount = Either +typealias MaybeTxHistoryItems = Either>> + +@Suppress("LongParameterList") +internal class TxHistorySubscriber( + private val userWallet: UserWallet, + private val isRefresh: Boolean, + private val stateHolder: WalletStateHolderV2, + private val clickIntents: WalletClickIntentsV2, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, +) : WalletSubscriber>(name = "tx_history") { + + override fun create( + coroutineScope: CoroutineScope, + uiDispatcher: CoroutineContext, + ): Flow> { + return flow { + getPrimaryCurrencyStatusUpdatesUseCase.collectLatest(userWalletId = userWallet.walletId) { status -> + val maybeTxHistoryItemCount = txHistoryItemsCountUseCase( + userWalletId = userWallet.walletId, + currency = status.currency, + ) + + setLoadingTxHistoryState(maybeTxHistoryItemCount, status) + + maybeTxHistoryItemCount.onRight { + val maybeTxHistoryItems = txHistoryItemsUseCase( + userWalletId = userWallet.walletId, + currency = status.currency, + refresh = isRefresh, + ).map { it.cachedIn(coroutineScope) } + + setLoadedTxHistoryState(maybeTxHistoryItems) + } + } + } + } + + private fun setLoadingTxHistoryState(maybeTxHistoryItemCount: MaybeTxHistoryCount, status: CryptoCurrencyStatus) { + stateHolder.update( + maybeTxHistoryItemCount.fold( + ifLeft = { + SetTxHistoryCountErrorTransformer( + userWallet = userWallet, + error = it, + pendingTransactions = status.value.pendingTransactions, + clickIntents = clickIntents, + ) + }, + ifRight = { + SetTxHistoryCountTransformer( + userWalletId = userWallet.walletId, + transactionsCount = it, + clickIntents = clickIntents, + ) + }, + ), + ) + } + + private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems) { + stateHolder.update( + maybeTxHistoryItems.fold( + ifLeft = { + SetTxHistoryItemsErrorTransformer( + userWalletId = userWallet.walletId, + error = it, + clickIntents = clickIntents, + ) + }, + ifRight = { + SetTxHistoryItemsTransformer( + userWallet = userWallet, + flow = it, + clickIntents = clickIntents, + ) + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt new file mode 100644 index 0000000000..112c7cbe8a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt @@ -0,0 +1,30 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import timber.log.Timber +import kotlin.coroutines.CoroutineContext + +/** + * Component for implementation of flow subscription + * + * @property name unique name of subscriber + * [T] - type of flow + * +[REDACTED_AUTHOR] + */ +internal abstract class WalletSubscriber(val name: String) { + + protected abstract fun create(coroutineScope: CoroutineScope, uiDispatcher: CoroutineContext): Flow + + fun subscribe(coroutineScope: CoroutineScope, dispatchers: CoroutineDispatcherProvider): Job { + Timber.d("Subscribe on $name") + return create(coroutineScope, dispatchers.main) + .flowOn(dispatchers.main) + .launchIn(coroutineScope) + } +} \ No newline at end of file From f89ba800e985e59e4cb8acc4bc501c75c7a15ac8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 15:49:34 +0800 Subject: [PATCH 09/20] Updated on 2026-08-14 --- .../loaders/WalletContentLoaderFactory.kt | 40 ++++++++ .../wallet/loaders/WalletLoaderStorage.kt | 26 ++++++ .../loaders/WalletScreenContentLoader.kt | 91 +++++++++++++++++++ .../implementors/MultiWalletContentLoader.kt | 46 ++++++++++ .../MultiWalletContentLoaderFactory.kt | 39 ++++++++ .../implementors/SingleWalletContentLoader.kt | 66 ++++++++++++++ .../SingleWalletContentLoaderFactory.kt | 51 +++++++++++ .../SingleWalletWithTokenContentLoader.kt | 46 ++++++++++ ...ngleWalletWithTokenContentLoaderFactory.kt | 37 ++++++++ .../implementors/WalletContentLoader.kt | 19 ++++ .../intents/WalletCardClickIntents.kt | 5 +- .../intents/WalletClickIntentsV2.kt | 39 ++++---- .../intents/WalletContentClickIntents.kt | 2 + .../WalletCurrencyActionsClickIntents.kt | 2 + .../intents/WalletWarningsClickIntents.kt | 2 + 15 files changed, 489 insertions(+), 22 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/WalletContentLoader.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt new file mode 100644 index 0000000000..a6d0db40dc --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt @@ -0,0 +1,40 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.MultiWalletContentLoaderFactory +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.SingleWalletContentLoaderFactory +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.SingleWalletWithTokenContentLoaderFactory +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.WalletContentLoader +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import dagger.hilt.android.scopes.ViewModelScoped +import javax.inject.Inject + +@ViewModelScoped +internal class WalletContentLoaderFactory @Inject constructor( + private val multiWalletContentLoaderFactory: MultiWalletContentLoaderFactory, + private val singleWalletWithTokenContentLoaderFactory: SingleWalletWithTokenContentLoaderFactory, + private val singleWalletContentLoaderFactory: SingleWalletContentLoaderFactory, +) { + + fun create( + userWallet: UserWallet, + appCurrency: AppCurrency, + clickIntents: WalletClickIntentsV2, + isRefresh: Boolean = false, + ): WalletContentLoader? { + return when { + userWallet.isMultiCurrency -> { + multiWalletContentLoaderFactory.create(userWallet, appCurrency, clickIntents) + } + userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() -> { + singleWalletWithTokenContentLoaderFactory.create(userWallet, appCurrency, clickIntents) + } + !userWallet.isMultiCurrency -> { + singleWalletContentLoaderFactory.create(userWallet, appCurrency, clickIntents, isRefresh) + } + else -> null + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt new file mode 100644 index 0000000000..11414466c8 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders + +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.Job +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class WalletLoaderStorage @Inject constructor() { + + private val loaders = ConcurrentHashMap>() + + fun contains(id: UserWalletId) = loaders.containsKey(id) + + fun set(id: UserWalletId, jobs: List) { + loaders[id] = jobs + } + + fun remove(id: UserWalletId) { + loaders[id]?.let { + it.forEach(Job::cancel) + loaders.remove(id) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt new file mode 100644 index 0000000000..24b6e550ed --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt @@ -0,0 +1,91 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.CoroutineScope +import timber.log.Timber +import javax.inject.Inject + +/** + * Base wallet screen content loader. Use it to load content by [UserWallet]. + * + * @property factory factory that creates loader + * @property storage storage that save loader's jobs + * @property dispatchers coroutine dispatchers provider + * +[REDACTED_AUTHOR] + */ +@ViewModelScoped +internal class WalletScreenContentLoader @Inject constructor( + private val factory: WalletContentLoaderFactory, + private val storage: WalletLoaderStorage, + private val dispatchers: CoroutineDispatcherProvider, +) { + + /** + * Load content by [UserWallet] + * + * @param userWallet user wallet + * @param appCurrency app currency + * @param clickIntents click intents + * @param isRefresh flag that determinate if content must load again + * @param coroutineScope coroutine scope + */ + fun load( + userWallet: UserWallet, + appCurrency: AppCurrency, + clickIntents: WalletClickIntentsV2, + isRefresh: Boolean = false, + coroutineScope: CoroutineScope, + ) { + if (userWallet.isLocked) return + + val id = userWallet.walletId + if (!storage.contains(id)) { + loadInternal(userWallet, appCurrency, clickIntents, coroutineScope, isRefresh) + } else { + if (isRefresh) { + storage.remove(id) + loadInternal(userWallet, appCurrency, clickIntents, coroutineScope, true) + } else { + Timber.d("$id content loading has already started") + } + } + } + + /** Cancel loading by [id] */ + fun cancel(id: UserWalletId) { + Timber.d("$id content loading is canceled") + storage.remove(id) + } + + private fun loadInternal( + userWallet: UserWallet, + appCurrency: AppCurrency, + clickIntents: WalletClickIntentsV2, + coroutineScope: CoroutineScope, + isRefresh: Boolean, + ) { + val loader = factory.create( + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + isRefresh = isRefresh, + ) + + if (loader == null) { + Timber.e("Impossible to create loader for $userWallet") + return + } + + Timber.d("${userWallet.walletId} content loading is ${if (isRefresh) "re" else ""}started") + + loader.subscribers + .map { it.subscribe(coroutineScope, dispatchers) } + .let { storage.set(userWallet.walletId, it) } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt new file mode 100644 index 0000000000..6424208a3f --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -0,0 +1,46 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.TokenListSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 + +@Suppress("LongParameterList") +internal class MultiWalletContentLoader( + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val clickIntents: WalletClickIntentsV2, + private val stateHolder: WalletStateHolderV2, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, + private val walletWithFundsChecker: WalletWithFundsChecker, + private val getTokenListUseCase: GetTokenListUseCase, + private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, +) : WalletContentLoader(id = userWallet.walletId) { + + override fun create(): List> { + return listOf( + TokenListSubscriber( + userWallet = userWallet, + appCurrency = appCurrency, + stateHolder = stateHolder, + clickIntents = clickIntents, + tokenListAnalyticsSender = tokenListAnalyticsSender, + walletWithFundsChecker = walletWithFundsChecker, + getTokenListUseCase = getTokenListUseCase, + ), + MultiWalletWarningsSubscriber( + userWalletId = userWallet.walletId, + stateHolder = stateHolder, + clickIntents = clickIntents, + getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt new file mode 100644 index 0000000000..1751d80a8b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -0,0 +1,39 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import dagger.hilt.android.scopes.ViewModelScoped +import javax.inject.Inject + +@ViewModelScoped +internal class MultiWalletContentLoaderFactory @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val getTokenListUseCase: GetTokenListUseCase, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, + private val walletWithFundsChecker: WalletWithFundsChecker, + private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, +) { + + fun create( + userWallet: UserWallet, + appCurrency: AppCurrency, + clickIntents: WalletClickIntentsV2, + ): WalletContentLoader { + return MultiWalletContentLoader( + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + stateHolder = stateHolder, + tokenListAnalyticsSender = tokenListAnalyticsSender, + walletWithFundsChecker = walletWithFundsChecker, + getTokenListUseCase = getTokenListUseCase, + getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt new file mode 100644 index 0000000000..792c45ee35 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt @@ -0,0 +1,66 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.subscribers.* +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 + +@Suppress("LongParameterList") +internal class SingleWalletContentLoader( + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val clickIntents: WalletClickIntentsV2, + private val isRefresh: Boolean, + private val stateHolder: WalletStateHolderV2, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, + private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, + private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, + private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, +) : WalletContentLoader(id = userWallet.walletId) { + + override fun create(): List> { + return listOf( + PrimaryCurrencySubscriber( + userWallet = userWallet, + appCurrency = appCurrency, + stateHolder = stateHolder, + getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, + setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase, + analyticsEventHandler = analyticsEventHandler, + ), + SingleWalletButtonsSubscriber( + userWallet = userWallet, + stateHolder = stateHolder, + clickIntents = clickIntents, + getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, + getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase, + ), + SingleWalletNotificationsSubscriber( + userWalletId = userWallet.walletId, + stateHolder = stateHolder, + clickIntents = clickIntents, + getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, + ), + TxHistorySubscriber( + userWallet = userWallet, + isRefresh = isRefresh, + stateHolder = stateHolder, + clickIntents = clickIntents, + getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, + txHistoryItemsCountUseCase = txHistoryItemsCountUseCase, + txHistoryItemsUseCase = txHistoryItemsUseCase, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt new file mode 100644 index 0000000000..05eb03ee25 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt @@ -0,0 +1,51 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import dagger.hilt.android.scopes.ViewModelScoped +import javax.inject.Inject + +@ViewModelScoped +@Suppress("LongParameterList") +internal class SingleWalletContentLoaderFactory @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, + private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, + private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, + private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, +) { + + fun create( + userWallet: UserWallet, + appCurrency: AppCurrency, + clickIntents: WalletClickIntentsV2, + isRefresh: Boolean, + ): WalletContentLoader { + return SingleWalletContentLoader( + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + isRefresh = isRefresh, + stateHolder = stateHolder, + getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, + getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase, + getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, + setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase, + txHistoryItemsCountUseCase = txHistoryItemsCountUseCase, + txHistoryItemsUseCase = txHistoryItemsUseCase, + analyticsEventHandler = analyticsEventHandler, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt new file mode 100644 index 0000000000..fd3242278a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -0,0 +1,46 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetCardTokensListUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletWithTokenListSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 + +@Suppress("LongParameterList") +internal class SingleWalletWithTokenContentLoader( + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val clickIntents: WalletClickIntentsV2, + private val stateHolder: WalletStateHolderV2, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, + private val walletWithFundsChecker: WalletWithFundsChecker, + private val getCardTokensListUseCase: GetCardTokensListUseCase, + private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, +) : WalletContentLoader(id = userWallet.walletId) { + + override fun create(): List> { + return listOf( + SingleWalletWithTokenListSubscriber( + userWallet = userWallet, + appCurrency = appCurrency, + stateHolder = stateHolder, + clickIntents = clickIntents, + tokenListAnalyticsSender = tokenListAnalyticsSender, + walletWithFundsChecker = walletWithFundsChecker, + getCardTokensListUseCase = getCardTokensListUseCase, + ), + MultiWalletWarningsSubscriber( + userWalletId = userWallet.walletId, + stateHolder = stateHolder, + clickIntents = clickIntents, + getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt new file mode 100644 index 0000000000..efef9eeb8d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt @@ -0,0 +1,37 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetCardTokensListUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import javax.inject.Inject + +internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, + private val walletWithFundsChecker: WalletWithFundsChecker, + private val getCardTokensListUseCase: GetCardTokensListUseCase, + private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, +) { + + fun create( + userWallet: UserWallet, + appCurrency: AppCurrency, + clickIntents: WalletClickIntentsV2, + ): SingleWalletWithTokenContentLoader { + return SingleWalletWithTokenContentLoader( + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + stateHolder = stateHolder, + tokenListAnalyticsSender = tokenListAnalyticsSender, + walletWithFundsChecker = walletWithFundsChecker, + getCardTokensListUseCase = getCardTokensListUseCase, + getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/WalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/WalletContentLoader.kt new file mode 100644 index 0000000000..83f9c28342 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/WalletContentLoader.kt @@ -0,0 +1,19 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber + +/** + * Wallet content loader + * + * @property id loader id + * +[REDACTED_AUTHOR] + */ +internal abstract class WalletContentLoader(val id: UserWalletId) { + + /** Loader's subscribers */ + val subscribers: List> get() = create() + + protected abstract fun create(): List> +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt index e7cc3d17a9..1bcff88b81 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt @@ -4,6 +4,7 @@ import com.tangem.core.navigation.AppScreen import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.DeleteWalletUseCase import com.tangem.domain.wallets.usecase.UpdateWalletUseCase +import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader 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.state.components.WalletCardState @@ -27,7 +28,7 @@ internal interface WalletCardClickIntents { internal class WalletCardClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateHolderV2, private val walletEventSender: WalletEventSender, - // TODO: private val walletScreenContentLoader: WalletScreenContentLoader, + private val walletScreenContentLoader: WalletScreenContentLoader, private val updateWalletUseCase: UpdateWalletUseCase, private val deleteWalletUseCase: DeleteWalletUseCase, private val dispatchers: CoroutineDispatcherProvider, @@ -51,7 +52,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) { viewModelScope.launch(dispatchers.main) { - // TODO: walletScreenContentLoader.cancel(userWalletId) + walletScreenContentLoader.cancel(userWalletId) deleteWalletUseCase(userWalletId) .onRight { popBackIfAllWalletsIsLocked() } .onLeft { Timber.e(it.toString()) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt index 643c5dec03..55e0a76bc5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.settings.NeverToShowWalletsScrollPreview import com.tangem.domain.tokens.FetchCardTokenListUseCase @@ -11,29 +12,29 @@ import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.analytics.PortfolioEvent import com.tangem.feature.wallet.presentation.wallet.domain.unwrap +import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state2.WalletState import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetRefreshStateTransformer import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTokenListErrorTransformer import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject -/** -[REDACTED_AUTHOR] - */ @Suppress("LongParameterList") +@ViewModelScoped internal class WalletClickIntentsV2 @Inject constructor( private val walletCardClickIntentsImplementor: WalletCardClickIntentsImplementor, private val warningsClickIntentsImplementer: WalletWarningsClickIntentsImplementer, private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor, private val contentClickIntentsImplementor: WalletContentClickIntentsImplementor, private val stateHolder: WalletStateHolderV2, - // TODO: private val walletScreenContentLoader: WalletScreenContentLoader, + private val walletScreenContentLoader: WalletScreenContentLoader, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val selectWalletUseCase: SelectWalletUseCase, - // TODO: private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val fetchTokenListUseCase: FetchTokenListUseCase, private val fetchCardTokenListUseCase: FetchCardTokenListUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, @@ -66,13 +67,12 @@ internal class WalletClickIntentsV2 @Inject constructor( stateHolder.update { it.copy(selectedWalletIndex = index) } maybeUserWallet.onRight { - // TODO: - // walletScreenContentLoader.load( - // userWallet = it, - // appCurrency = getSelectedAppCurrencyUseCase.unwrap(), - // clickIntents = this@WalletClickIntentsV2, - // coroutineScope = viewModelScope, - // ) + walletScreenContentLoader.load( + userWallet = it, + appCurrency = getSelectedAppCurrencyUseCase.unwrap(), + clickIntents = this@WalletClickIntentsV2, + coroutineScope = viewModelScope, + ) } } } @@ -133,14 +133,13 @@ internal class WalletClickIntentsV2 @Inject constructor( viewModelScope.launch(dispatchers.main) { fetchCurrencyStatusUseCase(userWallet.walletId, refresh = true) - // TODO: - // walletScreenContentLoader.load( - // userWallet = userWallet, - // appCurrency = getSelectedAppCurrencyUseCase.unwrap(), - // clickIntents = this@WalletClickIntentsV2, - // coroutineScope = viewModelScope, - // isRefresh = true, - // ) + walletScreenContentLoader.load( + userWallet = userWallet, + appCurrency = getSelectedAppCurrencyUseCase.unwrap(), + clickIntents = this@WalletClickIntentsV2, + coroutineScope = viewModelScope, + isRefresh = true, + ) stateHolder.update( SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = false), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt index 82c943e41b..a3582c6e63 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt @@ -19,6 +19,7 @@ import com.tangem.feature.wallet.presentation.wallet.state2.transformers.CloseBo import com.tangem.feature.wallet.presentation.wallet.state2.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.MultiWalletCurrencyActionsConverter import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch @@ -42,6 +43,7 @@ internal interface WalletContentClickIntents { } @Suppress("LongParameterList") +@ViewModelScoped internal class WalletContentClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateHolderV2, private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 0b6070971a..9690fa7684 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -37,6 +37,7 @@ import com.tangem.feature.wallet.presentation.wallet.state2.transformers.CloseBo import com.tangem.feature.wallet.presentation.wallet.state2.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.take @@ -65,6 +66,7 @@ interface WalletCurrencyActionsClickIntents { } @Suppress("LongParameterList", "LargeClass") +@ViewModelScoped internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateHolderV2, private val walletEventSender: WalletEventSender, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index b4b9b2d06f..cfa8be01ab 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -38,6 +38,7 @@ import com.tangem.feature.wallet.presentation.wallet.state2.transformers.OpenBot import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.launch import javax.inject.Inject @@ -63,6 +64,7 @@ internal interface WalletWarningsClickIntents { } @Suppress("LongParameterList") +@ViewModelScoped internal class WalletWarningsClickIntentsImplementer @Inject constructor( private val stateHolder: WalletStateHolderV2, private val walletEventSender: WalletEventSender, From 236b977e46d06909d5a5bcf0e2982f978264f6d7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 15:56:35 +0800 Subject: [PATCH 10/20] Updated on 2026-08-14 --- .../tokens/legacy/redux/TokensMiddleware.kt | 18 +- .../configs/feature_toggles_config.json | 4 + .../com/tangem/core/ui/event/EventEffect.kt | 5 +- .../featuretoggles/WalletFeatureToggles.kt | 2 + .../feature/wallet/di/WalletRouterModule.kt | 8 +- .../DefaultWalletFeatureToggles.kt | 3 + .../router/DefaultWalletRouter.kt | 23 +- .../wallet/viewmodels/WalletViewModel.kt | 9 +- .../wallet/viewmodels/WalletViewModelV2.kt | 311 ++++++++++++++++++ .../WalletsUpdateActionResolverV2.kt | 305 +++++++++++++++++ 10 files changed, 670 insertions(+), 18 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolverV2.kt diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt index 1ec868ce0e..101c7c51e5 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt @@ -20,6 +20,8 @@ import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.TokenWithBlockchain import com.tangem.domain.tokens.TokensAction import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.walletconnect.WalletConnectActions +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.* @@ -90,13 +92,17 @@ object TokensMiddleware { if (scanResponse.supportsHdWallet()) { deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) { submitNewAdd( - userWalletId = action.userWallet.walletId, + userWallet = action.userWallet, updatedScanResponse = it, currencyList = currencyList, ) } } else { - submitNewAdd(userWalletId = action.userWallet.walletId, scanResponse, currencyList = currencyList) + submitNewAdd( + userWallet = action.userWallet, + updatedScanResponse = scanResponse, + currencyList = currencyList, + ) } } } @@ -375,16 +381,18 @@ object TokensMiddleware { } private fun submitNewAdd( - userWalletId: UserWalletId, + userWallet: UserWallet, updatedScanResponse: ScanResponse, currencyList: List, ) { scope.launch { userWalletsListManager.update( - userWalletId = userWalletId, + userWalletId = userWallet.walletId, update = { it.copy(scanResponse = updatedScanResponse) }, ).doOnSuccess { - addCryptoCurrenciesUseCase(userWalletId, currencyList) + addCryptoCurrenciesUseCase(userWallet.walletId, currencyList).onRight { + store.dispatch(action = WalletConnectActions.New.SetupUserChains(userWallet = userWallet)) + } } } store.dispatchOnMain(NavigationAction.PopBackTo()) 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 9da64e96a0..106d635c23 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -34,5 +34,9 @@ { "name": "REDESIGNED_SEND_SCREEN_ENABLED", "version": "undefined" + }, + { + "name": "WALLETS_SCROLLING_PREVIEW_ENABLED", + "version": "5.4.0" } ] diff --git a/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt index e6f3ad0d84..516dba38e6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt @@ -3,6 +3,7 @@ package com.tangem.core.ui.event import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.NonRestartableComposable +import kotlinx.coroutines.launch /** * A Composable function that reacts to a given [StateEvent], executing the provided action only once when the event @@ -17,8 +18,8 @@ import androidx.compose.runtime.NonRestartableComposable fun EventEffect(event: StateEvent, onTrigger: suspend (data: A) -> Unit) { LaunchedEffect(event) { if (event is StateEvent.Triggered) { - onTrigger(event.data) - event.onConsume() + launch { onTrigger(event.data) } + .invokeOnCompletion { event.onConsume() } } } } \ No newline at end of file diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt index 5af5379a9e..9485b4d30c 100644 --- a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt @@ -9,4 +9,6 @@ interface WalletFeatureToggles { /** Availability of redesigned screen */ val isRedesignedScreenEnabled: Boolean + + val isWalletsScrollingPreviewEnabled: Boolean } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt index 1e64fc4122..81c4fd83ef 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.di import com.tangem.core.navigation.ReduxNavController import com.tangem.feature.wallet.presentation.router.DefaultWalletRouter +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.features.wallet.navigation.WalletRouter import dagger.Module import dagger.Provides @@ -15,7 +16,10 @@ internal object WalletRouterModule { @Provides @ActivityScoped - fun provideWalletRouter(reduxNavController: ReduxNavController): WalletRouter { - return DefaultWalletRouter(reduxNavController = reduxNavController) + fun provideWalletRouter( + reduxNavController: ReduxNavController, + walletFeatureToggles: WalletFeatureToggles, + ): WalletRouter { + return DefaultWalletRouter(reduxNavController = reduxNavController, walletFeatureToggles = walletFeatureToggles) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt index 71696ceba1..09655bdd61 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt @@ -16,4 +16,7 @@ internal class DefaultWalletFeatureToggles( override val isRedesignedScreenEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_WALLET_SCREEN_ENABLED") + + override val isWalletsScrollingPreviewEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "WALLETS_SCROLLING_PREVIEW_ENABLED") } \ No newline at end of file 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 646cec18f5..5dd19e4a33 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 @@ -26,12 +26,18 @@ import com.tangem.feature.wallet.presentation.WalletFragment import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen +import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreenV2 import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModelV2 import com.tangem.features.tokendetails.navigation.TokenDetailsRouter +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlin.properties.Delegates /** Default implementation of wallet feature router */ -internal class DefaultWalletRouter(private val reduxNavController: ReduxNavController) : InnerWalletRouter { +internal class DefaultWalletRouter( + private val reduxNavController: ReduxNavController, + private val walletFeatureToggles: WalletFeatureToggles, +) : InnerWalletRouter { private var navController: NavHostController by Delegates.notNull() private var onFinish: () -> Unit = {} @@ -47,10 +53,19 @@ internal class DefaultWalletRouter(private val reduxNavController: ReduxNavContr startDestination = WalletRoute.Wallet.route, ) { composable(WalletRoute.Wallet.route) { - val viewModel = hiltViewModel().apply { router = this@DefaultWalletRouter } - LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) + if (walletFeatureToggles.isWalletsScrollingPreviewEnabled) { + val viewModel = hiltViewModel().apply { + router = this@DefaultWalletRouter + } - WalletScreen(state = viewModel.uiState) + WalletScreen(state = viewModel.uiState) + } else { + val viewModel = hiltViewModel().apply { + setWalletRouter(router = this@DefaultWalletRouter) + } + + WalletScreenV2(state = viewModel.uiState.collectAsStateWithLifecycle().value) + } } composable( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index bc1c275b2f..1cb74caecf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -68,6 +68,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.* import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.factory.TokenListWithWallet import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletStateFactory +import com.tangem.feature.wallet.presentation.wallet.subscribers.MaybeTokenListFlow import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder @@ -129,10 +130,10 @@ internal class WalletViewModel @Inject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, private val scanCardToUnlockWalletUseCase: ScanCardToUnlockWalletClickHandler, - hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase, isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, isNeedToBackupUseCase: IsNeedToBackupUseCase, getMissedAddressesCryptoCurrenciesUseCase: GetMissedAddressesCryptoCurrenciesUseCase, + hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase, // endregion Parameters ) : ViewModel(), DefaultLifecycleObserver, WalletClickIntents { @@ -1352,7 +1353,7 @@ internal class WalletViewModel @Inject constructor( } } - private suspend fun updateButtons(userWallet: UserWallet, currencyStatus: CryptoCurrencyStatus) { + private fun updateButtons(userWallet: UserWallet, currencyStatus: CryptoCurrencyStatus) { getCryptoCurrencyActionsUseCase( userWallet = userWallet, cryptoCurrencyStatus = currencyStatus, @@ -1416,6 +1417,4 @@ internal class WalletViewModel @Inject constructor( } private fun getCardTypeResolver(index: Int): CardTypesResolver = getWallet(index).scanResponse.cardTypesResolver -} - -typealias MaybeTokenListFlow = Flow> \ No newline at end of file +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt new file mode 100644 index 0000000000..a9a2908965 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt @@ -0,0 +1,311 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import arrow.core.getOrElse +import com.tangem.common.Provider +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.settings.CanUseBiometryUseCase +import com.tangem.domain.settings.IsWalletsScrollPreviewEnabled +import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase +import com.tangem.domain.walletconnect.WalletConnectActions +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase +import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent +import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent.DemonstrateWalletsScrollPreview.Direction +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.* +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject +import kotlin.properties.Delegates + +@Suppress("LongParameterList") +@HiltViewModel +internal class WalletViewModelV2 @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val clickIntents: WalletClickIntentsV2, + private val walletEventSender: WalletEventSender, + private val walletsUpdateActionResolver: WalletsUpdateActionResolverV2, + private val walletScreenContentLoader: WalletScreenContentLoader, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + private val getWalletsUseCase: GetWalletsUseCase, + private val shouldShowSaveWalletScreenUseCase: ShouldShowSaveWalletScreenUseCase, + private val canUseBiometryUseCase: CanUseBiometryUseCase, + private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val isWalletsScrollPreviewEnabled: IsWalletsScrollPreviewEnabled, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + analyticsEventsHandler: AnalyticsEventHandler, + private val dispatchers: CoroutineDispatcherProvider, + private val reduxStateHolder: ReduxStateHolder, +) : ViewModel() { + + val uiState: StateFlow = stateHolder.uiState + + private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow() + + private var router: InnerWalletRouter by Delegates.notNull() + private var walletsUpdateJobHolder: JobHolder = JobHolder() + + init { + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened) + + suggestToEnableBiometrics() + + subscribeOnWalletsUpdateFlow() + subscribeOnBalanceHiding() + subscribeOnSelectedWalletFlow() + } + + fun setWalletRouter(router: InnerWalletRouter) { + this.router = router + clickIntents.initialize(router, viewModelScope) + } + + private fun suggestToEnableBiometrics() { + viewModelScope.launch(dispatchers.main) { + withContext(dispatchers.io) { delay(timeMillis = 1_800) } + + if (isShowSaveWalletScreenEnabled()) router.openSaveUserWalletScreen() + } + } + + private suspend fun isShowSaveWalletScreenEnabled(): Boolean { + return router.isWalletLastScreen() && shouldShowSaveWalletScreenUseCase() && canUseBiometryUseCase() + } + + private fun subscribeOnWalletsUpdateFlow() { + viewModelScope.launch(dispatchers.main) { + shouldSaveUserWalletsUseCase() + .conflate() + .distinctUntilChanged() + .collectLatest { shouldSaveUserWallet -> + getWalletsUseCase() + .distinctUntilChanged() + .conflate() + .map { + walletsUpdateActionResolver.resolve( + wallets = it, + currentState = stateHolder.value, + canSaveWallets = shouldSaveUserWallet, + ) + } + .onEach(::updateWallets) + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + .saveIn(walletsUpdateJobHolder) + } + } + } + + private fun subscribeOnBalanceHiding() { + getBalanceHidingSettingsUseCase() + .conflate() + .distinctUntilChanged() + .onEach { + stateHolder.update(transformer = UpdateBalanceHidingModeTransformer(it.isBalanceHidden)) + } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + } + + private fun subscribeOnSelectedWalletFlow() { + getSelectedWalletUseCase().onRight { + it + .conflate() + .distinctUntilChanged() + .onEach { selectedWallet -> + if (selectedWallet.isMultiCurrency) { + reduxStateHolder.dispatch( + action = WalletConnectActions.New.Initialize(userWallet = selectedWallet), + ) + } + } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + } + } + + private fun updateWallets(action: WalletsUpdateActionResolverV2.Action) { + when (action) { + is WalletsUpdateActionResolverV2.Action.InitializeWallets -> initializeWallets(action) + is WalletsUpdateActionResolverV2.Action.ReinitializeWallets -> { + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + appCurrency = selectedAppCurrencyFlow.value, + clickIntents = clickIntents, + coroutineScope = viewModelScope, + isRefresh = true, + ) + } + is WalletsUpdateActionResolverV2.Action.ReinitializeWallet -> reinitializeWallet(action) + is WalletsUpdateActionResolverV2.Action.AddWallet -> addWallet(action) + is WalletsUpdateActionResolverV2.Action.DeleteWallet -> deleteWallet(action) + is WalletsUpdateActionResolverV2.Action.UnlockWallet -> unlockWallet(action) + is WalletsUpdateActionResolverV2.Action.UpdateWalletCardCount -> { + stateHolder.update(transformer = UpdateWalletCardsCountTransformer(action.selectedWallet)) + } + is WalletsUpdateActionResolverV2.Action.UpdateWalletName -> { + stateHolder.update(transformer = RenameWalletTransformer(action.selectedWalletId, action.name)) + } + is WalletsUpdateActionResolverV2.Action.Unknown -> Unit + } + } + + private fun initializeWallets(action: WalletsUpdateActionResolverV2.Action.InitializeWallets) { + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + appCurrency = selectedAppCurrencyFlow.value, + clickIntents = clickIntents, + coroutineScope = viewModelScope, + ) + + stateHolder.update( + transformer = InitializeWalletsTransformer( + selectedWalletIndex = action.selectedWalletIndex, + selectedWallet = action.selectedWallet, + wallets = action.wallets, + clickIntents = clickIntents, + ), + ) + + viewModelScope.launch(dispatchers.main) { + if (action.wallets.size > 1 && isWalletsScrollPreviewEnabled()) { + withContext(dispatchers.io) { + delay(timeMillis = 1_800) + } + + walletEventSender.send( + event = WalletEvent.DemonstrateWalletsScrollPreview( + direction = if (action.selectedWalletIndex == action.wallets.lastIndex) { + Direction.RIGHT + } else { + Direction.LEFT + }, + ), + ) + } + } + } + + private fun reinitializeWallet(action: WalletsUpdateActionResolverV2.Action.ReinitializeWallet) { + viewModelScope.launch(dispatchers.main) { + walletScreenContentLoader.cancel(action.prevWalletId) + + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + appCurrency = selectedAppCurrencyFlow.value, + clickIntents = clickIntents, + coroutineScope = viewModelScope, + ) + + stateHolder.update( + ReinitializeWalletTransformer(userWallet = action.selectedWallet, clickIntents = clickIntents), + ) + } + } + + private fun addWallet(action: WalletsUpdateActionResolverV2.Action.AddWallet) { + viewModelScope.launch(dispatchers.main) { + stateHolder.update( + AddWalletTransformer( + userWallet = action.selectedWallet, + clickIntents = clickIntents, + ), + ) + + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + appCurrency = selectedAppCurrencyFlow.value, + clickIntents = clickIntents, + coroutineScope = viewModelScope, + ) + + withContext(dispatchers.io) { delay(timeMillis = 700) } + + scrollToWallet(index = action.selectedWalletIndex) + } + } + + private fun deleteWallet(action: WalletsUpdateActionResolverV2.Action.DeleteWallet) { + viewModelScope.launch(dispatchers.main) { + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + appCurrency = selectedAppCurrencyFlow.value, + clickIntents = clickIntents, + coroutineScope = viewModelScope, + ) + + scrollToWallet(index = action.selectedWalletIndex) + + withContext(dispatchers.io) { delay(timeMillis = 700) } + + stateHolder.update( + DeleteWalletTransformer( + selectedWalletIndex = action.selectedWalletIndex, + deletedWalletId = action.deletedWalletId, + ), + ) + } + } + + private fun unlockWallet(action: WalletsUpdateActionResolverV2.Action.UnlockWallet) { + viewModelScope.launch(dispatchers.main) { + withContext(dispatchers.io) { delay(timeMillis = 700) } + + stateHolder.update( + transformer = UnlockWalletTransformer( + unlockedWallets = action.unlockedWallets, + clickIntents = clickIntents, + ), + ) + + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + appCurrency = selectedAppCurrencyFlow.value, + clickIntents = clickIntents, + coroutineScope = viewModelScope, + ) + } + } + + private fun scrollToWallet(index: Int) { + stateHolder.update( + ScrollToWalletTransformer( + index = index, + currentStateProvider = Provider(action = stateHolder::value), + stateUpdater = { newState -> stateHolder.update { newState } }, + ), + ) + } + + private fun createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolverV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolverV2.kt new file mode 100644 index 0000000000..821148d80c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolverV2.kt @@ -0,0 +1,305 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +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 dagger.hilt.android.scopes.ViewModelScoped +import timber.log.Timber +import javax.inject.Inject + +/** + * Resolver that determines which update action will be performed + * + * @property getSelectedWalletSyncUseCase use case that returns selected wallet + */ +@ViewModelScoped +internal class WalletsUpdateActionResolverV2 @Inject constructor( + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, +) { + + private var isInitialized: Boolean = false + private var canSaveWallets: Boolean = false + + fun resolve(wallets: List, currentState: WalletScreenState, canSaveWallets: Boolean): Action { + val selectedWallet = wallets.getSelectedWallet() ?: return Action.Unknown + + val action = when { + isFirstInitialization(currentState) -> { + createInitializeWalletsAction(wallets, selectedWallet, canSaveWallets) + } + isReinitialization(canSaveWallets) -> { + this.canSaveWallets = canSaveWallets + Action.ReinitializeWallets(selectedWallet = selectedWallet) + } + else -> getUpdateContentAction(currentState, wallets, selectedWallet) + } + + Timber.d("Resolved action: $action") + + return action + } + + private fun List.getSelectedWallet(): UserWallet? { + return when { + isEmpty() -> null + size == 1 -> first() + else -> getSelectedWalletSyncUseCase().fold(ifLeft = { null }, ifRight = { it }) + } + } + + private fun isFirstInitialization(state: WalletScreenState): Boolean { + return state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX + } + + private fun createInitializeWalletsAction( + wallets: List, + selectedWallet: UserWallet, + canSaveWallets: Boolean, + ): Action { + this.isInitialized = true + this.canSaveWallets = canSaveWallets + + return Action.InitializeWallets( + selectedWalletIndex = wallets.indexOfWallet(selectedWallet.walletId), + selectedWallet = selectedWallet, + wallets = wallets, + ) + } + + private fun isReinitialization(canSaveWallets: Boolean): Boolean { + return isInitialized && this.canSaveWallets != canSaveWallets + } + + private fun getUpdateContentAction( + state: WalletScreenState, + wallets: List, + selectedWallet: UserWallet, + ): Action { + return when { + isWalletsCountChanged(state, wallets) -> { + getChangeWalletsListAction(state, wallets, selectedWallet) + } + isAnotherWalletSelected(state, selectedWallet) -> { + Action.ReinitializeWallet( + prevWalletId = state.getPrevSelectedWallet().id, + selectedWallet = selectedWallet, + ) + } + else -> getUpdateSelectedWalletAction(state, wallets, selectedWallet) + } + } + + private fun isWalletsCountChanged(state: WalletScreenState, wallets: List): Boolean { + val prevWalletsSize = state.wallets.size + val walletsSize = wallets.size + + return prevWalletsSize != walletsSize + } + + private fun getChangeWalletsListAction( + state: WalletScreenState, + wallets: List, + selectedWallet: UserWallet, + ): Action { + val prevWalletsSize = state.wallets.size + + return when { + prevWalletsSize > wallets.size -> { + Action.DeleteWallet( + selectedWallet = selectedWallet, + selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId), + deletedWalletId = state.wallets.getDeletedWalletId(wallets), + ) + } + prevWalletsSize < wallets.size -> { + val newUserWallet = state.wallets.getAddedWallet(wallets) + Action.AddWallet( + selectedWalletIndex = wallets.indexOfWallet(id = newUserWallet.walletId), + selectedWallet = newUserWallet, + ) + } + else -> error("Wallets list is not changed") + } + } + + private fun List.getDeletedWalletId(wallets: List): UserWalletId { + return this + .map { it.walletCardState.id } + .firstOrNull { !wallets.map(UserWallet::walletId).contains(it) } + ?: error("Deleted wallet id is not found. Wallets contains all previous wallets ids") + } + + private fun List.getAddedWallet(wallets: List): UserWallet { + return wallets + .firstOrNull { wallet -> !this.map { it.walletCardState.id }.contains(wallet.walletId) } + ?: error("Added wallet id is not found. Wallets contains all previous wallets ids") + } + + private fun isAnotherWalletSelected(state: WalletScreenState, selectedWallet: UserWallet): Boolean { + return state.getPrevSelectedWallet().id != selectedWallet.walletId + } + + private fun getUpdateSelectedWalletAction( + state: WalletScreenState, + wallets: List, + selectedWallet: UserWallet, + ): Action { + return when { + isSelectedWalletNameChanged(state, selectedWallet) -> { + Action.UpdateWalletName(selectedWalletId = selectedWallet.walletId, name = selectedWallet.name) + } + isSelectedWalletUnlocked(state, selectedWallet) -> { + Action.UnlockWallet( + selectedWallet = selectedWallet, + unlockedWallets = wallets.filterNot(UserWallet::isLocked), + ) + } + isSelectedWalletCardsCountChanged(state, selectedWallet) -> Action.UpdateWalletCardCount(selectedWallet) + else -> Action.Unknown + } + } + + private fun isSelectedWalletNameChanged(state: WalletScreenState, selectedWallet: UserWallet): Boolean { + return state.getPrevSelectedWallet().title != selectedWallet.name + } + + private fun isSelectedWalletUnlocked(state: WalletScreenState, selectedWallet: UserWallet): Boolean { + return state.isSelectedWalletLocked() && !selectedWallet.isLocked + } + + private fun isSelectedWalletCardsCountChanged(state: WalletScreenState, selectedWallet: UserWallet): Boolean { + val prevSelectedWallet = state.getPrevSelectedWallet() + return prevSelectedWallet is WalletCardState.Content && + prevSelectedWallet.cardCount != selectedWallet.getCardsCount() + } + + private fun WalletScreenState.isSelectedWalletLocked(): Boolean { + val selectedWalletState = wallets.getOrNull(selectedWalletIndex) ?: error("Selected wallet is not found") + return selectedWalletState is WalletState.MultiCurrency.Locked || + selectedWalletState is WalletState.SingleCurrency.Locked + } + + private fun WalletScreenState.getPrevSelectedWallet(): WalletCardState { + return wallets + .map(WalletState::walletCardState) + .getOrNull(selectedWalletIndex) + ?: error("Previous selected wallet is not found") + } + + private fun List.indexOfWallet(id: UserWalletId): Int { + val selectedIndex = indexOfFirst { it.walletId == id } + + return if (selectedIndex == -1) { + error("Wallets don't contain a wallet with id: $id") + } else { + selectedIndex + } + } + + sealed class Action { + + data class InitializeWallets( + val selectedWalletIndex: Int, + val selectedWallet: UserWallet, + val wallets: List, + ) : Action() { + + override fun toString(): String { + return """ + Initialize( + selectedWalletIndex=$selectedWalletIndex, + selectedWallet=${selectedWallet.walletId}, + wallets=${wallets.joinToString { it.walletId.toString() }} + ) + """.trimIndent() + } + } + + /** + * Reinitialize wallets. Example, if user turned on wallets saving + * + * @property selectedWallet selected wallet + */ + data class ReinitializeWallets(val selectedWallet: UserWallet) : Action() { + + override fun toString(): String { + return "Reinitialize(selectedWallet=${selectedWallet.walletId})" + } + } + + /** + * Reinitialize selected wallet. Example, scanning a new card if wallets saving is turned off + * + * @property prevWalletId previous selected wallet id + * @property selectedWallet selected wallet + */ + data class ReinitializeWallet(val prevWalletId: UserWalletId, val selectedWallet: UserWallet) : Action() { + + override fun toString(): String { + return "ReinitializeWallet(prevWalletId=$prevWalletId, selectedWallet=${selectedWallet.walletId})" + } + } + + data class UpdateWalletName(val selectedWalletId: UserWalletId, val name: String) : Action() { + + override fun toString(): String { + return "UpdateWalletName(selectedWalletId=$selectedWalletId, name=$name)" + } + } + + data class UnlockWallet(val selectedWallet: UserWallet, val unlockedWallets: List) : Action() { + + override fun toString(): String { + return """ + UnlockWallet( + selectedWallet=${selectedWallet.walletId}, + unlockedWallets=${unlockedWallets.joinToString { it.walletId.toString() }} + ) + """.trimIndent() + } + } + + data class DeleteWallet( + val selectedWallet: UserWallet, + val selectedWalletIndex: Int, + val deletedWalletId: UserWalletId, + ) : Action() { + + override fun toString(): String { + return """ + DeleteWallet( + selectedWallet=${selectedWallet.walletId}, + selectedWalletIndex=$selectedWalletIndex, + deletedWalletId=$deletedWalletId + ) + """.trimIndent() + } + } + + data class AddWallet(val selectedWalletIndex: Int, val selectedWallet: UserWallet) : Action() { + + override fun toString(): String { + return "AddWallet(selectedWalletIndex=$selectedWalletIndex, selectedWallet=${selectedWallet.walletId})" + } + } + + /** + * Update wallet card count. Example, if user backed up wallet + * + * @property selectedWallet selected wallet + */ + data class UpdateWalletCardCount(val selectedWallet: UserWallet) : Action() { + + override fun toString(): String { + return "UpdateWalletCardCount(selectedWallet=${selectedWallet.walletId})" + } + } + + object Unknown : Action() + } +} \ No newline at end of file From 93880c648228b5ff0dee4241d4392ec81d5538c4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 23:23:10 +0400 Subject: [PATCH 11/20] Updated on 2026-08-14 --- .../local/preferences/AppPreferencesStore.kt | 33 ++++++++++-- .../utils/AppPreferencesStoreExt.kt | 52 ++++++++++++++++--- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt index 7e401b9333..ae7bb4ee8c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt @@ -5,6 +5,7 @@ import androidx.datastore.preferences.core.MutablePreferences import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import com.squareup.moshi.Moshi +import com.squareup.moshi.Types /** * Application preferences store. @@ -31,15 +32,39 @@ class AppPreferencesStore( return edit { transform(it) } } - /** Get nullable data [T] by string [key] from [MutablePreferences] */ + /** + * Get nullable data [T] by string [key] from [MutablePreferences] + * + * Warning: This method cannot be used for T with parameterized types (e.g. List, Set, etc.). + * + * @see getObjectList + * */ inline fun MutablePreferences.getObject(key: Preferences.Key): T? { - val adapter = moshi.adapter(T::class.java) + val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types return this[key]?.let(adapter::fromJson) } - /** Set data [T] by string [key] to [MutablePreferences] */ + /** Get nullable list of data [T] by string [key] */ + inline fun MutablePreferences.getObjectList(key: Preferences.Key): List? { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) + return this[key]?.let(adapter::fromJson) + } + + /** + * Set data [T] by string [key] to [MutablePreferences] + * + * Warning: This method cannot be used for T with parameterized types (e.g. List, Set, etc.). + * + * @see setObjectList + * */ inline fun MutablePreferences.setObject(key: Preferences.Key, value: T) { - val adapter = moshi.adapter(T::class.java) + val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types + this[key] = adapter.toJson(value) + } + + /** Set list of data [T] by string [key] to [MutablePreferences] */ + inline fun MutablePreferences.setObjectList(key: Preferences.Key, value: List) { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) this[key] = adapter.toJson(value) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt index 1ca1355871..974b36147a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.local.preferences.utils import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit +import com.squareup.moshi.Types import com.tangem.datasource.local.preferences.AppPreferencesStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull @@ -13,15 +14,27 @@ inline fun AppPreferencesStore.getObject(key: Preferences.Key AppPreferencesStore.getObject(key: Preferences.Key, default: T): Flow { - val adapter = moshi.adapter(T::class.java) + val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types return data.map { it[key]?.let(adapter::fromJson) ?: default } } -/** Get nullable data [T] by string [key] */ +/** + * Get nullable data [T] by string [key] + * + * Warning: This method cannot be used for T with parameterized types (e.g. List, Set, etc.). + * + * @see getObjectListSync + * */ suspend inline fun AppPreferencesStore.getObjectSyncOrNull(key: Preferences.Key): T? { - val adapter = moshi.adapter(T::class.java) + val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types return data.firstOrNull() ?.get(key) ?.let(adapter::fromJson) @@ -39,8 +52,35 @@ suspend inline fun AppPreferencesStore.getObjectSyncOrDefault( ?: default } -/** Store data [value] by string [key] */ +/** + * Store data [value] by string [key] + * + * Warning: This method cannot be used for T with parameterized types (e.g. List, Set, etc.). + * + * @see storeObjectList + * */ suspend inline fun AppPreferencesStore.storeObject(key: Preferences.Key, value: T) { - val adapter = moshi.adapter(T::class.java) + val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types edit { it[key] = adapter.toJson(value) } +} + +/** Store list of data [value] by string [key] */ +suspend inline fun AppPreferencesStore.storeObjectList(key: Preferences.Key, value: List) { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) + edit { it[key] = adapter.toJson(value) } +} + +/** Get flow of list of data [T] by string [key]. If data is not found, it returns `null` */ +inline fun AppPreferencesStore.getObjectList(key: Preferences.Key): Flow?> { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) + return data.map { it[key]?.let(adapter::fromJson) } +} + +/** Get nullable list of data [T] by string [key] */ +suspend inline fun AppPreferencesStore.getObjectListSync(key: Preferences.Key): List { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) + return data.firstOrNull() + ?.get(key) + ?.let(adapter::fromJson) + .orEmpty() } \ No newline at end of file From 507c09beff593569aaa8d965ba35e23dec483cc0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 23:23:42 +0400 Subject: [PATCH 12/20] Updated on 2026-08-14 --- .../java/com/tangem/data/card/DefaultCardRepository.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt index 7f0a56fd9a..84c73a8dc7 100644 --- a/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt @@ -3,7 +3,7 @@ package com.tangem.data.card import com.tangem.datasource.local.card.UsedCardInfo import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObject +import com.tangem.datasource.local.preferences.utils.getObjectList import com.tangem.domain.card.repository.CardRepository import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow @@ -14,7 +14,7 @@ internal class DefaultCardRepository( ) : CardRepository { override fun wasCardScanned(cardId: String): Flow { - return appPreferencesStore.getObject>(key = PreferencesKeys.USED_CARDS_INFO_KEY) + return appPreferencesStore.getObjectList(key = PreferencesKeys.USED_CARDS_INFO_KEY) .map { savedCards -> savedCards?.any { it.cardId == cardId } ?: false } @@ -22,14 +22,14 @@ internal class DefaultCardRepository( override suspend fun setCardWasScanned(cardId: String) { appPreferencesStore.editData { mutablePreferences -> - val usedCards: List? = mutablePreferences.getObject( + val usedCards: List? = mutablePreferences.getObjectList( key = PreferencesKeys.USED_CARDS_INFO_KEY, ) val updatedUsedCards = usedCards?.updateCard(cardId) ?: listOf(UsedCardInfo(cardId = cardId, isScanned = true)) - mutablePreferences.setObject( + mutablePreferences.setObjectList( key = PreferencesKeys.USED_CARDS_INFO_KEY, value = updatedUsedCards, ) From d58ac113f5efb9dd323adbe1a8171317f0975e15 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Nov 2023 15:44:02 +0400 Subject: [PATCH 13/20] Updated on 2026-08-14 --- .../components/SettingsSwitchItem.kt | 2 +- .../bottomsheets/TangemBottomSheet.kt | 4 +- .../common/ui/ChooseWalletBottomSheet.kt | 22 +++ .../managetokens/state/ManageTokensState.kt | 24 +++ .../ManageTokensStatePreviewData.kt | 40 +++++ .../ui/ChooseNetworkBottomSheet.kt | 24 +++ .../managetokens/ui/ManageTokensScreen.kt | 144 ++++++++++++++++++ .../ui/components/AddCustomTokenButton.kt | 65 ++++++++ .../managetokens/ui/components/TokensList.kt | 59 +++++++ 9 files changed, 382 insertions(+), 2 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletBottomSheet.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkBottomSheet.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt index b584136754..e4198a521c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt @@ -14,11 +14,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.SpacerH4 import com.tangem.core.ui.components.SpacerW32 +import com.tangem.core.ui.components.TangemSwitch import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item -import com.tangem.core.ui.components.TangemSwitch @Composable internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt index 2d9d85b164..76a172ffd9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt @@ -6,6 +6,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color import com.tangem.core.ui.res.TangemTheme import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch @@ -20,6 +21,7 @@ import kotlinx.coroutines.launch @Composable inline fun TangemBottomSheet( config: TangemBottomSheetConfig, + contentColor: Color = TangemTheme.colors.background.primary, crossinline content: @Composable ColumnScope.(T) -> Unit, ) { var isVisible by remember { mutableStateOf(value = config.isShow) } @@ -29,7 +31,7 @@ inline fun TangemBottomSheet( ModalBottomSheet( onDismissRequest = config.onDismissRequest, sheetState = sheetState, - containerColor = TangemTheme.colors.background.primary, + containerColor = contentColor, shape = TangemTheme.shapes.bottomSheetLarge, dragHandle = { TangemBottomSheetDraggableHeader() }, ) { diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletBottomSheet.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletBottomSheet.kt new file mode 100644 index 0000000000..610f54edc4 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletBottomSheet.kt @@ -0,0 +1,22 @@ +package com.tangem.managetokens.presentation.common.ui + +import androidx.compose.runtime.Composable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.managetokens.presentation.common.state.ChooseWalletState + +@Composable +internal fun ChooseWalletBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + contentColor = TangemTheme.colors.background.tertiary, + ) { + ChooseWalletScreen(state = it.chooseWalletState) + } +} + +internal class ChooseWalletBottomSheetConfig( + val chooseWalletState: ChooseWalletState.Choose, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt new file mode 100644 index 0000000000..1ee0ecebce --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt @@ -0,0 +1,24 @@ +package com.tangem.managetokens.presentation.managetokens.state + +import androidx.paging.PagingData +import com.tangem.core.ui.event.StateEvent +import com.tangem.managetokens.presentation.common.state.ChooseWalletState +import com.tangem.managetokens.presentation.common.state.Event +import kotlinx.coroutines.flow.Flow + +internal data class ManageTokensState( + val searchBarState: SearchBarState, + val tokens: Flow>, + val isLoading: Boolean, + val addCustomTokenButton: AddCustomTokenButton, + val chooseWalletState: ChooseWalletState, + val derivationNotification: DerivationNotificationState? = null, + val selectedToken: TokenItemState.Loaded? = null, + val showChooseWalletScreen: Boolean = false, + val event: StateEvent, +) + +data class AddCustomTokenButton( + val isVisible: Boolean, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt new file mode 100644 index 0000000000..6ea8316ba5 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt @@ -0,0 +1,40 @@ +package com.tangem.managetokens.presentation.managetokens.state.previewdata + +import androidx.paging.PagingData +import com.tangem.core.ui.event.consumedEvent +import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData +import com.tangem.managetokens.presentation.managetokens.state.AddCustomTokenButton +import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState +import com.tangem.managetokens.presentation.managetokens.state.SearchBarState +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState +import kotlinx.coroutines.flow.flowOf + +internal object ManageTokensStatePreviewData { + val loadedState: ManageTokensState + get() = ManageTokensState( + searchBarState = searchState, + tokens = flowOf(PagingData.from(tokens)), + isLoading = false, + addCustomTokenButton = AddCustomTokenButton(true, {}), + derivationNotification = DerivationNotificationStatePreviewData.state, + event = consumedEvent(), + chooseWalletState = ChooseWalletStatePreviewData.state, + ) + + val loadingState: ManageTokensState + get() = loadedState.copy(isLoading = true) + + private val tokens: List + get() = listOf( + TokenItemStatePreviewData.loadedPriceDown, + TokenItemStatePreviewData.loadedPriceUp, + ) + + private val searchState: SearchBarState + get() = SearchBarState( + query = "", + onQueryChange = {}, + active = false, + onActiveChange = {}, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkBottomSheet.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkBottomSheet.kt new file mode 100644 index 0000000000..cd1809a96b --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkBottomSheet.kt @@ -0,0 +1,24 @@ +package com.tangem.managetokens.presentation.managetokens.ui + +import androidx.compose.runtime.Composable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.managetokens.presentation.common.state.ChooseWalletState +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState + +@Composable +internal fun ChooseNetworkBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + contentColor = TangemTheme.colors.background.tertiary, + ) { + ChooseNetworkScreen(state = it.selectedToken, walletState = it.chooseWalletState) + } +} + +internal class ChooseNetworkBottomSheetConfig( + val selectedToken: TokenItemState.Loaded, + val chooseWalletState: ChooseWalletState, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt new file mode 100644 index 0000000000..1bbf0c61a9 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt @@ -0,0 +1,144 @@ +package com.tangem.managetokens.presentation.managetokens.ui + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Surface +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.paging.compose.collectAsLazyPagingItems +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.res.TangemTheme +import com.tangem.managetokens.presentation.common.state.AlertState +import com.tangem.managetokens.presentation.common.state.ChooseWalletState +import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheet +import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheetConfig +import com.tangem.managetokens.presentation.common.ui.EventEffect +import com.tangem.managetokens.presentation.common.ui.components.Alert +import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState +import com.tangem.managetokens.presentation.managetokens.state.previewdata.ManageTokensStatePreviewData +import com.tangem.managetokens.presentation.managetokens.ui.components.DerivationNotification +import com.tangem.managetokens.presentation.managetokens.ui.components.TokensList +import com.tangem.managetokens.presentation.managetokens.ui.components.TokensSearchBar + +@Composable +internal fun ManageTokensScreen(state: ManageTokensState) { + var alertState by remember { mutableStateOf(value = null) } + + EventEffect( + event = state.event, + onAlertStateSet = { alertState = it }, + ) + alertState?.let { + Alert(state = it, onDismiss = { alertState = null }) + } + + Content(state) +} + +@Composable +private fun Content(state: ManageTokensState) { + Box( + modifier = Modifier + .fillMaxSize() + .background(color = TangemTheme.colors.background.primary) + .padding(top = TangemTheme.dimens.spacing32), + ) { + Column { + val listState = rememberLazyListState() + val raiseSearchBar by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } + + val elevation by animateDpAsState( + targetValue = if (raiseSearchBar) { + TangemTheme.dimens.elevation8 + } else { + TangemTheme.dimens.elevation0 + }, + label = "top_bar_elevation", + ) + Surface( + elevation = elevation, + modifier = Modifier, + ) { + TokensSearchBar( + state = state.searchBarState, + modifier = Modifier + .background(color = TangemTheme.colors.background.primary) + .padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing20), + ) + } + val tokens = state.tokens.collectAsLazyPagingItems() + TokensList(tokens = tokens, addCustomTokenButton = state.addCustomTokenButton) + } + state.derivationNotification?.let { + DerivationNotification( + config = it.config, + modifier = Modifier + .align(Alignment.BottomCenter), + ) + } + state.selectedToken?.let { selectedToken -> + ManageTokensBottomSheet(selectedToken = selectedToken, state = state) + } + } +} + +@Composable +private fun ManageTokensBottomSheet(selectedToken: TokenItemState.Loaded, state: ManageTokensState) { + if (state.showChooseWalletScreen && state.chooseWalletState is ChooseWalletState.Choose) { + val config = TangemBottomSheetConfig( + isShow = true, + content = ChooseWalletBottomSheetConfig(state.chooseWalletState), + onDismissRequest = state.chooseWalletState.onCloseChoosingWalletClick, + ) + ChooseWalletBottomSheet(config) + } else { + val config = TangemBottomSheetConfig( + isShow = true, + content = ChooseNetworkBottomSheetConfig( + selectedToken = selectedToken, + chooseWalletState = state.chooseWalletState, + ), + onDismissRequest = selectedToken.chooseNetworkState.onCloseChooseNetworkScreen, + ) + ChooseNetworkBottomSheet(config) + } +} + +@Preview +@Composable +private fun Preview_ManageTokensScreen_LightTheme( + @PreviewParameter(ManageTokensConfigProvider::class) + state: ManageTokensState, +) { + TangemTheme(isDark = false) { + ManageTokensScreen(state) + } +} + +@Preview +@Composable +private fun Preview_ManageTokensScreen_DarkTheme( + @PreviewParameter(ManageTokensConfigProvider::class) + state: ManageTokensState, +) { + TangemTheme(isDark = true) { + ManageTokensScreen(state) + } +} + +private class ManageTokensConfigProvider : CollectionPreviewParameterProvider( + collection = listOf( + ManageTokensStatePreviewData.loadingState, + ManageTokensStatePreviewData.loadedState, + ), +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt new file mode 100644 index 0000000000..195bb4d60c --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt @@ -0,0 +1,65 @@ +package com.tangem.managetokens.presentation.managetokens.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.managetokens.impl.R + +@Composable +internal fun AddCustomTokenButton(onButtonClick: () -> Unit, modifier: Modifier = Modifier) { + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .defaultMinSize(minHeight = TangemTheme.dimens.size68) + .fillMaxWidth() + .clickable { onButtonClick() } + .padding(horizontal = TangemTheme.dimens.spacing16), + ) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size36) + .background(color = TangemTheme.colors.button.secondary, shape = CircleShape), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_plus_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } + SpacerW(width = TangemTheme.dimens.spacing12) + Text( + text = stringResource(id = R.string.add_custom_token_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Preview +@Composable +private fun AddCustomTokenButton_Preview_Light() { + TangemTheme(isDark = false) { + AddCustomTokenButton(onButtonClick = { }) + } +} + +@Preview +@Composable +private fun AddCustomTokenButton_Preview_Dark() { + TangemTheme(isDark = true) { + AddCustomTokenButton(onButtonClick = { }) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt new file mode 100644 index 0000000000..fe6c1c2bf7 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt @@ -0,0 +1,59 @@ +package com.tangem.managetokens.presentation.managetokens.ui.components + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.managetokens.state.AddCustomTokenButton +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState + +private const val PLACEHOLDER_ITEMS_COUNT = 50 + +@Composable +internal fun TokensList(tokens: LazyPagingItems, addCustomTokenButton: AddCustomTokenButton) { + LazyColumn { + item { + Text( + text = stringResource(id = R.string.manage_tokens_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) + } + + if (tokens.loadState.refresh is LoadState.Loading) { + items(PLACEHOLDER_ITEMS_COUNT) { + TokenRowItem(state = TokenItemState.Loading(it.toString())) + } + } else { + val tokensList = tokens.itemSnapshotList + if (tokensList.isEmpty()) { + item { + Text( + text = stringResource(id = R.string.manage_tokens_nothing_found), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) + } + } + + items(items = tokensList.items, key = TokenItemState::id) { token -> + TokenRowItem(state = token) + } + + if (addCustomTokenButton.isVisible) { + item { + AddCustomTokenButton(onButtonClick = { addCustomTokenButton.onClick }) + } + } + } + } +} \ No newline at end of file From 45193e4878a9d28321604f492c67f628414cd57b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 29 Nov 2023 20:20:08 +0300 Subject: [PATCH 14/20] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings-blockchain.xml | 1 - core/res/src/main/res/values-fr/strings-blockchain.xml | 1 - core/res/src/main/res/values-it/strings-blockchain.xml | 1 - core/res/src/main/res/values-zh-rTW/strings-blockchain.xml | 1 - 4 files changed, 4 deletions(-) diff --git a/core/res/src/main/res/values-de/strings-blockchain.xml b/core/res/src/main/res/values-de/strings-blockchain.xml index b6c425deeb..d438a29cd8 100644 --- a/core/res/src/main/res/values-de/strings-blockchain.xml +++ b/core/res/src/main/res/values-de/strings-blockchain.xml @@ -1,7 +1,6 @@ Erhalt der Gebühr fehlgeschlagen - Laden Sie %1$s+ %2$s auf um ein Konto zu erstellen Minimaler Betrag ist %s Restbestand zu klein Falsche Gebühr diff --git a/core/res/src/main/res/values-fr/strings-blockchain.xml b/core/res/src/main/res/values-fr/strings-blockchain.xml index 42c3c19b03..0f4d6dccec 100644 --- a/core/res/src/main/res/values-fr/strings-blockchain.xml +++ b/core/res/src/main/res/values-fr/strings-blockchain.xml @@ -1,7 +1,6 @@ Échec de réception des commissions - Pour créer un compte, téléchargez %1$s+ %2$s Le montant minimal est de %s Le reste est trop petit Commission non valide diff --git a/core/res/src/main/res/values-it/strings-blockchain.xml b/core/res/src/main/res/values-it/strings-blockchain.xml index 2d50632130..338c03f0da 100644 --- a/core/res/src/main/res/values-it/strings-blockchain.xml +++ b/core/res/src/main/res/values-it/strings-blockchain.xml @@ -1,7 +1,6 @@ Impossibile ottenere la commissione - Scarica %1$s+ %2$s per creare un account L\'importo minimo è di %s L\'importo residuo è molto basso Commissione non valida diff --git a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml index 5aa30cef8f..607268ec3a 100644 --- a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml +++ b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml @@ -4,7 +4,6 @@ 遺留資產 獲取費用失敗 由於 Kaspa 的限制,只有%1$d UTXO 可以放入單次交易中。這意味著您只能發送%2$s或更少數量。您需要減少數量。 - 加載 %1$s+ %2$s 以創建帳戶 目標帳戶未激活。發送 %s 或更多以激活帳戶 最小數量是 %s 更動太小 From ed5d5b1b33b59f5a7282047c8aa271d2b3b10f4c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 30 Nov 2023 10:36:39 +0400 Subject: [PATCH 15/20] Updated on 2026-08-14 --- .../middlewares/TradeCryptoMiddleware.kt | 11 ++- .../chooseaddress/ChooseAddressBottomSheet.kt | 3 +- .../tokenreceive/AddressMappers.kt | 43 ++++++++++ .../bottomsheets/tokenreceive/AddressModel.kt | 5 +- .../tokenreceive/TokenReceiveBottomSheet.kt | 12 +-- .../TokenReceiveBottomSheetConfig.kt | 3 +- .../core/ui/extensions/BlockchainIcons.kt | 1 - .../data/tokens/utils/NetworkStatusFactory.kt | 24 +++++- .../domain/common/extensions/Blockchain.kt | 5 +- .../walletmanager/WalletManagersFacade.kt | 6 ++ .../domain/walletmanager/model/Address.kt | 11 +++ .../model/UpdateWalletManagerResult.kt | 8 +- .../utils/UpdateWalletManagerResultFactory.kt | 30 ++++--- .../tokens/model/CryptoCurrencyStatus.kt | 8 +- .../domain/tokens/model/NetworkAddress.kt | 30 ++++--- ...PrimaryCurrencyStatusUpdatesUseCaseTest.kt | 4 +- .../tangem/domain/tokens/mock/MockNetworks.kt | 16 +++- .../domain/tokens/mock/MockTokensStates.kt | 17 +++- .../state/factory/TokenDetailsStateFactory.kt | 27 ++---- .../viewmodels/TokenDetailsViewModel.kt | 46 +++++----- .../wallet/viewmodels/WalletViewModel.kt | 84 ++++++++----------- .../WalletCurrencyActionsClickIntents.kt | 58 ++++++------- gradle/dependencies.toml | 4 +- 23 files changed, 269 insertions(+), 187 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressMappers.kt create mode 100644 domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/Address.kt diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index c3092a3714..33a23f9750 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -14,6 +14,7 @@ import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.features.send.api.navigation.SendRouter import com.tangem.tap.common.analytics.events.AnalyticsParam @@ -123,7 +124,10 @@ class TradeCryptoMiddleware { } private fun proceedNewBuyAction(state: () -> AppState?, action: TradeCryptoAction.New.Buy) { - val networkAddress = action.cryptoCurrencyStatus.value.networkAddress?.defaultAddress ?: return + val networkAddress = action.cryptoCurrencyStatus.value.networkAddress + ?.defaultAddress + ?.let(NetworkAddress.Address::value) + ?: return val status = action.cryptoCurrencyStatus val currency = status.currency @@ -206,7 +210,10 @@ class TradeCryptoMiddleware { } private fun proceedNewSellAction(action: TradeCryptoAction.New.Sell) { - val networkAddress = action.cryptoCurrencyStatus.value.networkAddress?.defaultAddress ?: return + val networkAddress = action.cryptoCurrencyStatus.value.networkAddress + ?.defaultAddress + ?.let(NetworkAddress.Address::value) + ?: return val currency = action.cryptoCurrencyStatus.currency store.state.globalState.exchangeManager.getUrl( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/chooseaddress/ChooseAddressBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/chooseaddress/ChooseAddressBottomSheet.kt index 4bec68b142..098950828e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/chooseaddress/ChooseAddressBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/chooseaddress/ChooseAddressBottomSheet.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.SimpleSettingsRow import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @Composable @@ -24,7 +25,7 @@ private fun ChooseAddressBottomSheetContent(content: ChooseAddressBottomSheetCon ) { content.addressModels.forEach { addressModel -> SimpleSettingsRow( - title = addressModel.type.name, + title = addressModel.displayName.resolveReference(), icon = R.drawable.ic_arrow_top_right_24, onItemsClick = { content.onClick(addressModel) }, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressMappers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressMappers.kt new file mode 100644 index 0000000000..dfd23f44c2 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressMappers.kt @@ -0,0 +1,43 @@ +package com.tangem.core.ui.components.bottomsheets.tokenreceive + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkAddress + +private const val DSC_ADDRESS_NAME = "DSC" +private const val DEL_ADDRESS_NAME = "Main" + +private const val LEGACY_ADDRESS_NAME = "Legacy" +private const val DEFAULT_ADDRESS_NAME = "Default" + +fun Set.mapToAddressModels(currency: CryptoCurrency): List = this + .sortedBy { it.type.ordinal } + .map { address -> + AddressModel( + displayName = currency.network.getAddressDisplayName(address.type), + value = address.value, + type = when (address.type) { + NetworkAddress.Address.Type.Primary -> AddressModel.Type.Default + NetworkAddress.Address.Type.Secondary -> AddressModel.Type.Legacy + }, + ) + } + +private fun Network.getAddressDisplayName(addressType: NetworkAddress.Address.Type): TextReference { + return when (id.value) { + "decimal", "decimal/test" -> { + when (addressType) { + NetworkAddress.Address.Type.Primary -> stringReference(value = DEL_ADDRESS_NAME) + NetworkAddress.Address.Type.Secondary -> stringReference(value = DSC_ADDRESS_NAME) + } + } + else -> { + when (addressType) { + NetworkAddress.Address.Type.Primary -> stringReference(value = DEFAULT_ADDRESS_NAME) + NetworkAddress.Address.Type.Secondary -> stringReference(value = LEGACY_ADDRESS_NAME) + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressModel.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressModel.kt index 0314b21846..8571fdfa35 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressModel.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressModel.kt @@ -1,8 +1,11 @@ package com.tangem.core.ui.components.bottomsheets.tokenreceive +import com.tangem.core.ui.extensions.TextReference + data class AddressModel( + val displayName: TextReference, val value: String, - val type: Type = Type.Default, + val type: Type, ) { enum class Type { Legacy, Default diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt index 704418b5ec..1057a43847 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt @@ -9,7 +9,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.* +import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -26,6 +26,7 @@ import com.tangem.core.ui.components.SecondaryButtonIconStart import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.rememberQrPainters +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.shareText import com.tangem.core.ui.res.TangemTheme @@ -181,13 +182,6 @@ private fun getName(content: TokenReceiveBottomSheetConfig, index: Int): String return if (content.addresses.size < 2) { content.name } else { - "${ - stringResource( - id = when (content.addresses[index].type) { - AddressModel.Type.Default -> R.string.address_type_default - AddressModel.Type.Legacy -> R.string.address_type_legacy - }, - ) - } ${content.name}" + "${content.addresses[index].displayName.resolveReference()} ${content.name}" } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt index c2531a839d..a0a649302c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt @@ -1,12 +1,13 @@ package com.tangem.core.ui.components.bottomsheets.tokenreceive import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import kotlinx.collections.immutable.ImmutableList class TokenReceiveBottomSheetConfig( val name: String, val symbol: String, val network: String, - val addresses: List, + val addresses: ImmutableList, val onCopyClick: () -> Unit, val onShareClick: () -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index e418da8ea3..95a8e5e94e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -28,7 +28,6 @@ fun getActiveIconRes(blockchainId: String): Int { "GNO" -> R.drawable.img_gnosis_22 "ETH-Pow", "ETH-Pow/test" -> R.drawable.img_eth_pow_22 "ETH-Fair" -> R.drawable.img_eth_fair_22 - "NEAR", "NEAR/test" -> R.drawable.img_near_22 "Polkadot", "Polkadot/test" -> R.drawable.img_polkadot_22 "Kusama" -> R.drawable.img_kusama_22 "OPTIMISM", "OPTIMISM/test" -> R.drawable.img_optimism_22 diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt index 47979ec3a4..dbbf3fb10a 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt @@ -2,6 +2,7 @@ package com.tangem.data.tokens.utils import com.tangem.domain.tokens.model.* import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.walletmanager.model.Address import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult @@ -20,12 +21,12 @@ internal class NetworkStatusFactory { is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount( - address = getNetworkAddress(result.defaultAddress, result.addresses), + address = getNetworkAddress(result.selectedAddress, result.addresses), amountToCreateAccount = result.amountToCreateAccount, errorMessage = result.errorMessage, ) is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified( - address = getNetworkAddress(result.defaultAddress, result.addresses), + address = getNetworkAddress(result.selectedAddress, result.addresses), amounts = formatAmounts(result.currenciesAmounts, currencies), pendingTransactions = formatTransactions( transactions = result.currentTransactions, @@ -92,11 +93,26 @@ internal class NetworkStatusFactory { return transactions.mapTo(hashSetOf()) { it.txHistoryItem } } - private fun getNetworkAddress(defaultAddress: String, availableAddresses: Set): NetworkAddress { + private fun getNetworkAddress(selectedAddress: String, availableAddresses: Set
): NetworkAddress { + val defaultAddress = availableAddresses + .firstOrNull { it.value == selectedAddress } + ?.let(::mapToDomainAddress) + + requireNotNull(defaultAddress) { "Selected address must not be null" } + return if (availableAddresses.size != 1) { - NetworkAddress.Selectable(defaultAddress, availableAddresses) + NetworkAddress.Selectable(defaultAddress, availableAddresses.mapTo(hashSetOf(), ::mapToDomainAddress)) } else { NetworkAddress.Single(defaultAddress) } } + + private fun mapToDomainAddress(address: Address): NetworkAddress.Address { + val type = when (address.type) { + Address.Type.Primary -> NetworkAddress.Address.Type.Primary + Address.Type.Secondary -> NetworkAddress.Address.Type.Secondary + } + + return NetworkAddress.Address(address.value, type) + } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index 2e72d3e67f..e4fb4f23a1 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -195,8 +195,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Chia, Blockchain.ChiaTestnet -> "chia" Blockchain.Near -> "near" Blockchain.NearTestnet -> "near/test" - Blockchain.Decimal -> "decimal" - Blockchain.DecimalTestnet -> "decimal/test" + Blockchain.Decimal, Blockchain.DecimalTestnet -> "decimal" Blockchain.Unknown -> "unknown" } } @@ -224,6 +223,4 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5 private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Ducatus, - Blockchain.Decimal, - Blockchain.DecimalTestnet, ) \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index f2b89b51a6..dba5d089eb 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -116,6 +116,7 @@ interface WalletManagersFacade { * @param userWalletId selected wallet id * @param network network of currency */ + @Deprecated("Use NetworkAddress from CryptoCurrencyStatus") suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
/** Returns list of all addresses for all currencies in selected wallet @@ -123,6 +124,7 @@ interface WalletManagersFacade { * @param userWalletId selected wallet id * @param network required to create wallet manager */ + @Deprecated("Use NetworkAddress from CryptoCurrencyStatus") suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set
/** @@ -156,6 +158,7 @@ interface WalletManagersFacade { * @param userWalletId selected wallet id * @param network network of currency */ + @Deprecated("Will be removed in future") suspend fun getFee( amount: Amount, destination: String, @@ -171,6 +174,7 @@ interface WalletManagersFacade { * @param userWalletId selected wallet id * @param network network of currency */ + @Deprecated("Will be removed in future") suspend fun validateTransaction( amount: Amount, fee: Amount?, @@ -189,6 +193,7 @@ interface WalletManagersFacade { * @param network network of currency */ @Suppress("LongParameterList") + @Deprecated("Will be removed in future") suspend fun createTransaction( amount: Amount, fee: Fee, @@ -206,6 +211,7 @@ interface WalletManagersFacade { * @param userWalletId selected wallet id * @param network network of currency */ + @Deprecated("Will be removed in future") suspend fun sendTransaction( txData: TransactionData, signer: CommonSigner, diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/Address.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/Address.kt new file mode 100644 index 0000000000..c495bf7c9a --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/Address.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.walletmanager.model + +data class Address( + val value: String, + val type: Type, +) { + + enum class Type { + Primary, Secondary, + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt index ae14d752e1..dd1891105e 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt @@ -9,15 +9,15 @@ sealed class UpdateWalletManagerResult { object Unreachable : UpdateWalletManagerResult() data class Verified( - val defaultAddress: String, - val addresses: Set, + val selectedAddress: String, + val addresses: Set
, val currenciesAmounts: Set, val currentTransactions: Set, ) : UpdateWalletManagerResult() data class NoAccount( - val defaultAddress: String, - val addresses: Set, + val selectedAddress: String, + val addresses: Set
, val amountToCreateAccount: BigDecimal, val errorMessage: String, ) : UpdateWalletManagerResult() diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt index 1e9c032f6c..4a67053404 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt @@ -1,15 +1,17 @@ package com.tangem.domain.walletmanager.utils import com.tangem.blockchain.common.* -import com.tangem.blockchain.common.address.Address +import com.tangem.blockchain.common.address.AddressType import com.tangem.domain.common.extensions.amountToCreateAccount import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.walletmanager.model.Address import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import timber.log.Timber import java.math.BigDecimal import java.util.concurrent.TimeUnit +import com.tangem.blockchain.common.address.Address as SdkAddress internal class UpdateWalletManagerResultFactory { @@ -18,7 +20,7 @@ internal class UpdateWalletManagerResultFactory { val addresses = getAvailableAddresses(wallet.addresses) return UpdateWalletManagerResult.Verified( - defaultAddress = wallet.address, + selectedAddress = wallet.address, addresses = addresses, currenciesAmounts = getTokensAmounts(wallet.amounts.values.toSet()), currentTransactions = getCurrentTransactions(addresses, wallet.recentTransactions.toSet()), @@ -30,7 +32,7 @@ internal class UpdateWalletManagerResultFactory { val addresses = getAvailableAddresses(wallet.addresses) return UpdateWalletManagerResult.Verified( - defaultAddress = wallet.address, + selectedAddress = wallet.address, addresses = addresses, currenciesAmounts = getDemoTokensAmounts(demoAmount, walletManager.cardTokens), currentTransactions = getCurrentTransactions(addresses, wallet.recentTransactions.toSet()), @@ -48,7 +50,7 @@ internal class UpdateWalletManagerResultFactory { UpdateWalletManagerResult.Unreachable } else { UpdateWalletManagerResult.NoAccount( - defaultAddress = wallet.address, + selectedAddress = wallet.address, addresses = getAvailableAddresses(wallet.addresses), amountToCreateAccount = amountToCreateAccount, errorMessage = customMessage, @@ -70,7 +72,7 @@ internal class UpdateWalletManagerResultFactory { } private fun getCurrentTransactions( - walletAddresses: Set, + walletAddresses: Set
, recentTransactions: Set, ): Set { val unconfirmedTransactions = recentTransactions.filter { @@ -95,7 +97,7 @@ internal class UpdateWalletManagerResultFactory { } private fun createCurrencyTransaction( - walletAddresses: Set, + walletAddresses: Set
, data: TransactionData, ): CryptoCurrencyTransaction? { return when (val type = data.amount.type) { @@ -115,11 +117,11 @@ internal class UpdateWalletManagerResultFactory { } } - private fun createTxHistoryItem(walletAddresses: Set, data: TransactionData): TxHistoryItem? { + private fun createTxHistoryItem(walletAddresses: Set
, data: TransactionData): TxHistoryItem? { val hash = data.hash ?: return null val millis = data.date?.timeInMillis ?: return null val amount = getTransactionAmountValue(data.amount) ?: return null - val isOutgoing = data.sourceAddress in walletAddresses + val isOutgoing = data.sourceAddress in walletAddresses.map { it.value } return TxHistoryItem( txHash = hash, @@ -141,8 +143,16 @@ internal class UpdateWalletManagerResultFactory { ) } - private fun getAvailableAddresses(addresses: Set
): Set { - return addresses.mapTo(hashSetOf()) { it.value } + private fun getAvailableAddresses(addresses: Set): Set
{ + return addresses.mapTo(hashSetOf()) { sdkAddress -> + Address( + value = sdkAddress.value, + type = when (sdkAddress.type) { + AddressType.Default -> Address.Type.Primary + AddressType.Legacy -> Address.Type.Secondary + }, + ) + } } private fun getCurrencyAmountValue(amount: Amount): BigDecimal? { diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index 5ba1af68eb..23700e3d7b 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -78,7 +78,7 @@ data class CryptoCurrencyStatus( override val fiatAmount: BigDecimal?, override val priceChange: BigDecimal?, override val fiatRate: BigDecimal?, - override val networkAddress: NetworkAddress?, + override val networkAddress: NetworkAddress, ) : Status(isError = false) { override val amount: BigDecimal = BigDecimal.ZERO @@ -102,7 +102,7 @@ data class CryptoCurrencyStatus( override val priceChange: BigDecimal, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, - override val networkAddress: NetworkAddress?, + override val networkAddress: NetworkAddress, ) : Status(isError = false) /** @@ -123,7 +123,7 @@ data class CryptoCurrencyStatus( override val priceChange: BigDecimal?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, - override val networkAddress: NetworkAddress?, + override val networkAddress: NetworkAddress, ) : Status(isError = false) /** @@ -138,6 +138,6 @@ data class CryptoCurrencyStatus( override val amount: BigDecimal, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, - override val networkAddress: NetworkAddress?, + override val networkAddress: NetworkAddress, ) : Status(isError = false) } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkAddress.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkAddress.kt index 542d48485e..70d50581d4 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkAddress.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkAddress.kt @@ -6,18 +6,19 @@ package com.tangem.domain.tokens.model sealed class NetworkAddress { /** The default or currently selected network address. */ - abstract val defaultAddress: String + abstract val defaultAddress: Address + + /** The set of available network addresses to choose from. */ + abstract val availableAddresses: Set
/** * Represents a single static network address. * * @property defaultAddress The static network address. */ - data class Single(override val defaultAddress: String) : NetworkAddress() { + data class Single(override val defaultAddress: Address) : NetworkAddress() { - init { - checkDefaultAddress() - } + override val availableAddresses: Set
= setOf(defaultAddress) } /** @@ -27,17 +28,26 @@ sealed class NetworkAddress { * @property availableAddresses The set of available network addresses to choose from. */ data class Selectable( - override val defaultAddress: String, - val availableAddresses: Set, + override val defaultAddress: Address, + override val availableAddresses: Set
, ) : NetworkAddress() { init { - checkDefaultAddress() require(availableAddresses.isNotEmpty()) { "Available network addresses must not be empty" } } } - protected fun checkDefaultAddress() { - require(defaultAddress.isNotBlank()) { "Selected network address must not be blank" } + data class Address( + val value: String, + val type: Type, + ) { + + enum class Type { + Primary, Secondary, + } + + init { + require(value.isNotBlank()) { "Address value must not be blank" } + } } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index 6cdc76877c..b5c027ac68 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -118,7 +118,9 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { amount = BigDecimal.TEN, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), - networkAddress = NetworkAddress.Single(defaultAddress = "mock"), + networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), + ), ), ) } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt index 979479e010..369bb8b5b3 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -51,7 +51,9 @@ internal object MockNetworks { network = network3, value = NetworkStatus.NoAccount( amountToCreateAccount = amountToCreateAccount, - address = NetworkAddress.Single(defaultAddress = "mock"), + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), + ), errorMessage = "", ), ) @@ -67,7 +69,9 @@ internal object MockNetworks { MockTokens.token3.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN), ), pendingTransactions = emptyMap(), - address = NetworkAddress.Single(defaultAddress = "mock"), + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), + ), ), ) @@ -80,7 +84,9 @@ internal object MockNetworks { MockTokens.token6.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN), ), pendingTransactions = emptyMap(), - address = NetworkAddress.Single(defaultAddress = "mock"), + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), + ), ), ) @@ -94,7 +100,9 @@ internal object MockNetworks { MockTokens.token10.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN), ), pendingTransactions = emptyMap(), - address = NetworkAddress.Single(defaultAddress = "mock"), + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), + ), ), ) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index 38338ba598..bb9071f999 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -3,6 +3,7 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptyListOf import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.model.NetworkStatus import java.math.BigDecimal @@ -64,7 +65,9 @@ internal object MockTokensStates { priceChange = MockQuotes.quote7.priceChange, fiatRate = MockQuotes.quote7.fiatRate, amountToCreateAccount = MockNetworks.amountToCreateAccount, - networkAddress = null, + networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), + ), ), ) @@ -75,7 +78,9 @@ internal object MockTokensStates { priceChange = MockQuotes.quote8.priceChange, fiatRate = MockQuotes.quote8.fiatRate, amountToCreateAccount = MockNetworks.amountToCreateAccount, - networkAddress = null, + networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), + ), ), ) @@ -86,7 +91,9 @@ internal object MockTokensStates { priceChange = MockQuotes.quote9.priceChange, fiatRate = MockQuotes.quote9.fiatRate, amountToCreateAccount = MockNetworks.amountToCreateAccount, - networkAddress = null, + networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), + ), ), ) @@ -97,7 +104,9 @@ internal object MockTokensStates { priceChange = MockQuotes.quote10.priceChange, fiatRate = MockQuotes.quote10.fiatRate, amountToCreateAccount = MockNetworks.amountToCreateAccount, - networkAddress = null, + networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), + ), ), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 1494ed003f..997a6e985a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -2,12 +2,11 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import androidx.paging.PagingData import arrow.core.Either -import com.tangem.blockchain.common.address.Address import com.tangem.common.Provider import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.consumedEvent @@ -17,6 +16,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.txhistory.models.TxHistoryItem @@ -174,7 +174,7 @@ internal class TokenDetailsStateFactory( fun getStateWithReceiveBottomSheet( currency: CryptoCurrency, - addresses: List
, + networkAddress: NetworkAddress, sendCopyAnalyticsEvent: () -> Unit, sendShareAnalyticsEvent: () -> Unit, ): TokenDetailsState { @@ -186,12 +186,7 @@ internal class TokenDetailsStateFactory( name = currency.name, symbol = currency.symbol, network = currency.network.name, - addresses = addresses.map { - AddressModel( - value = it.value, - type = AddressModel.Type.valueOf(it.type.name), - ) - }, + addresses = networkAddress.availableAddresses.mapToAddressModels(currency).toImmutableList(), onCopyClick = sendCopyAnalyticsEvent, onShareClick = sendShareAnalyticsEvent, ), @@ -199,20 +194,16 @@ internal class TokenDetailsStateFactory( ) } - fun getStateWithChooseAddressBottomSheet(addresses: List
): TokenDetailsState { + fun getStateWithChooseAddressBottomSheet( + currency: CryptoCurrency, + networkAddress: NetworkAddress, + ): TokenDetailsState { return currentStateProvider().copy( bottomSheetConfig = TangemBottomSheetConfig( isShow = true, onDismissRequest = clickIntents::onDismissBottomSheet, content = ChooseAddressBottomSheetConfig( - addressModels = addresses - .map { address -> - AddressModel( - value = address.value, - type = AddressModel.Type.valueOf(address.type.name), - ) - } - .toImmutableList(), + addressModels = networkAddress.availableAddresses.mapToAddressModels(currency).toImmutableList(), onClick = clickIntents::onAddressTypeSelected, ), ), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index b1496b2e0c..7be83bff9e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -23,12 +23,12 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -67,7 +67,6 @@ internal class TokenDetailsViewModel @Inject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, - private val walletManagersFacade: WalletManagersFacade, private val isDemoCardUseCase: IsDemoCardUseCase, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventsHandler: AnalyticsEventHandler, @@ -303,19 +302,15 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onReceiveClick() { - analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrency.symbol)) + val networkAddress = cryptoCurrencyStatus?.value?.networkAddress ?: return viewModelScope.launch(dispatchers.io) { - val addresses = walletManagersFacade.getAddress( - userWalletId = userWalletId, - network = cryptoCurrency.network, - ) - - analyticsEventsHandler.send(event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened) + analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrency.symbol)) + analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened) uiState = stateFactory.getStateWithReceiveBottomSheet( currency = cryptoCurrency, - addresses = addresses, + networkAddress = networkAddress, sendCopyAnalyticsEvent = { analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) }, @@ -378,22 +373,23 @@ internal class TokenDetailsViewModel @Inject constructor( } private fun openExplorer() { - viewModelScope.launch(dispatchers.io) { - val addresses = walletManagersFacade.getAddress( - userWalletId = userWalletId, - network = cryptoCurrency.network, - ) + val currencyStatus = cryptoCurrencyStatus ?: return - if (addresses.size == 1) { - router.openUrl( - url = getExploreUrlUseCase( - userWalletId = userWalletId, - currency = cryptoCurrency, - addressType = AddressType.Default, - ), - ) - } else { - uiState = stateFactory.getStateWithChooseAddressBottomSheet(addresses = addresses) + viewModelScope.launch(dispatchers.io) { + when (val addresses = currencyStatus.value.networkAddress) { + is NetworkAddress.Selectable -> { + uiState = stateFactory.getStateWithChooseAddressBottomSheet(cryptoCurrency, addresses) + } + is NetworkAddress.Single -> { + router.openUrl( + url = getExploreUrlUseCase( + userWalletId = userWalletId, + currency = cryptoCurrency, + addressType = AddressType.Default, + ), + ) + } + null -> Unit } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 1cb74caecf..6534df2c8c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -19,6 +19,7 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.WrappedList @@ -42,10 +43,7 @@ import com.tangem.domain.settings.* import com.tangem.domain.tokens.* import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.NetworkGroup -import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase @@ -689,20 +687,11 @@ internal class WalletViewModel @Inject constructor( } override fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - val state = uiState as? WalletState.ContentState ?: return - analyticsEventsHandler.send( event = TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrencyStatus.currency.symbol), ) viewModelScope.launch(dispatchers.io) { - val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex) - - val addresses = walletManagersFacade.getAddress( - userWalletId = userWallet.walletId, - network = cryptoCurrencyStatus.currency.network, - ) - analyticsEventsHandler.send(event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened) val currency = cryptoCurrencyStatus.currency @@ -711,12 +700,11 @@ internal class WalletViewModel @Inject constructor( name = currency.name, symbol = currency.symbol, network = currency.network.name, - addresses = addresses.map { - AddressModel( - value = it.value, - type = AddressModel.Type.valueOf(it.type.name), - ) - }, + addresses = cryptoCurrencyStatus.value.networkAddress + ?.availableAddresses + ?.mapToAddressModels(currency) + .orEmpty() + .toImmutableList(), onCopyClick = { analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) }, @@ -821,41 +809,39 @@ internal class WalletViewModel @Inject constructor( private fun openExplorer() { val state = uiState as? WalletState.ContentState ?: return - val currency = singleWalletCryptoCurrencyStatus?.currency ?: return + val currencyStatus = singleWalletCryptoCurrencyStatus ?: return + val currency = currencyStatus.currency viewModelScope.launch(dispatchers.main) { val userWalletId = getWallet(state.walletsListConfig.selectedWalletIndex).walletId - val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network) - - if (addresses.size == 1) { - router.openUrl( - url = getExploreUrlUseCase( - userWalletId = userWalletId, - currency = currency, - addressType = AddressType.Default, - ), - ) - } else { - uiState = stateFactory.getStateWithOpenWalletBottomSheet( - ChooseAddressBottomSheetConfig( - addressModels = addresses - .map { address -> - AddressModel( - value = address.value, - type = AddressModel.Type.valueOf(address.type.name), + when (val addresses = currencyStatus.value.networkAddress) { + is NetworkAddress.Selectable -> { + uiState = stateFactory.getStateWithOpenWalletBottomSheet( + ChooseAddressBottomSheetConfig( + addressModels = addresses.availableAddresses + .mapToAddressModels(currency) + .toImmutableList(), + onClick = { + onAddressTypeSelected( + userWalletId = userWalletId, + currency = currency, + addressModel = it, ) - } - .toImmutableList(), - onClick = { - onAddressTypeSelected( - userWalletId = userWalletId, - currency = currency, - addressModel = it, - ) - }, - ), - ) + }, + ), + ) + } + is NetworkAddress.Single -> { + router.openUrl( + url = getExploreUrlUseCase( + userWalletId = userWalletId, + currency = currency, + addressType = AddressType.Default, + ), + ) + } + null -> Unit } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 9690fa7684..789557c99e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -1,12 +1,12 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents -import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels import com.tangem.core.ui.extensions.WrappedList import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -20,6 +20,7 @@ import com.tangem.domain.tokens.RemoveCurrencyUseCase import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.walletconnect.WalletConnectActions @@ -141,15 +142,15 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) viewModelScope.launch(dispatchers.main) { - val currency = cryptoCurrencyStatus.currency - val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network) - analyticsEventHandler.send(event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened) stateHolder.update( OpenBottomSheetTransformer( userWalletId = userWalletId, - content = createReceiveBottomSheetContent(currency, addresses), + content = createReceiveBottomSheetContent( + currency = cryptoCurrencyStatus.currency, + addresses = cryptoCurrencyStatus.value.networkAddress?.availableAddresses ?: return@launch, + ), onDismissBottomSheet = { stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) }, @@ -160,18 +161,13 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private fun createReceiveBottomSheetContent( currency: CryptoCurrency, - addresses: List
, + addresses: Set, ): TangemBottomSheetConfigContent { return TokenReceiveBottomSheetConfig( name = currency.name, symbol = currency.symbol, network = currency.network.name, - addresses = addresses.map { address -> - AddressModel( - value = address.value, - type = AddressModel.Type.valueOf(address.type.name), - ) - }, + addresses = addresses.mapToAddressModels(currency).toImmutableList(), onCopyClick = { analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) }, @@ -329,40 +325,36 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( val userWalletId = stateHolder.getSelectedWalletId() viewModelScope.launch(dispatchers.main) { - val currency = getPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId)?.currency ?: return@launch - val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network) + val currencyStatus = getPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId) ?: return@launch - if (addresses.size == 1) { - router.openUrl( - url = getExploreUrlUseCase( - userWalletId = userWalletId, - currency = currency, - addressType = AddressType.Default, - ), - ) - } else { - showChooseAddressBottomSheet(userWalletId, addresses, currency) + when (val addresses = currencyStatus.value.networkAddress) { + is NetworkAddress.Selectable -> { + showChooseAddressBottomSheet(userWalletId, addresses.availableAddresses, currencyStatus.currency) + } + is NetworkAddress.Single -> { + router.openUrl( + url = getExploreUrlUseCase( + userWalletId = userWalletId, + currency = currencyStatus.currency, + addressType = AddressType.Default, + ), + ) + } + null -> Unit } } } private fun showChooseAddressBottomSheet( userWalletId: UserWalletId, - addresses: List
, + addresses: Set, currency: CryptoCurrency, ) { stateHolder.update( OpenBottomSheetTransformer( userWalletId = userWalletId, content = ChooseAddressBottomSheetConfig( - addressModels = addresses - .map { address -> - AddressModel( - value = address.value, - type = AddressModel.Type.valueOf(address.type.name), - ) - } - .toImmutableList(), + addressModels = addresses.mapToAddressModels(currency).toImmutableList(), onClick = { onAddressTypeSelected( userWalletId = userWalletId, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 95e9a29cf5..fb9f8dad5f 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,10 +88,10 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-396" +tangemBlockchainSdk = "develop-401" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-312" -#tangemCardSdk = "0.0.1" # Keep it! - used for local builds +#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem # region Tools From 97f9283cea52862be282044136c9040c7ab82716 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 30 Nov 2023 12:17:43 +0300 Subject: [PATCH 16/20] Updated on 2026-08-14 --- .../presentation/router/DefaultWalletRouter.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 5dd19e4a33..e5ad37394c 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 @@ -54,17 +54,17 @@ internal class DefaultWalletRouter( ) { composable(WalletRoute.Wallet.route) { if (walletFeatureToggles.isWalletsScrollingPreviewEnabled) { - val viewModel = hiltViewModel().apply { - router = this@DefaultWalletRouter - } - - WalletScreen(state = viewModel.uiState) - } else { val viewModel = hiltViewModel().apply { setWalletRouter(router = this@DefaultWalletRouter) } WalletScreenV2(state = viewModel.uiState.collectAsStateWithLifecycle().value) + } else { + val viewModel = hiltViewModel().apply { + router = this@DefaultWalletRouter + } + + WalletScreen(state = viewModel.uiState) } } From 44494e9ba6f0942609c8740542ac66a2f4b4b9d9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 28 Nov 2023 16:43:57 +0100 Subject: [PATCH 17/20] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 11 ++- .../core/ui/extensions/CryptoCurrency.kt | 10 +++ data/tokens/build.gradle.kts | 1 + .../tangem/data/tokens/di/TokensDataModule.kt | 24 ++++-- .../data/tokens/paging/CoinsPagingSource.kt | 77 +++++++++++++++++++ .../tokens/paging/CoinsResponseConverter.kt | 51 ++++++++++++ .../repository/DefaultTokensListRepository.kt | 45 +++++++++++ .../data/tokens/utils/NetworkOperations.kt | 2 +- domain/tokens/build.gradle.kts | 3 + .../com/tangem/domain/tokens/model/Quote.kt | 7 +- .../com/tangem/domain/tokens/model/Token.kt | 41 ++++++++++ .../tokens/GetGlobalTokenListUseCase.kt | 22 ++++++ .../tokens/repository/TokensListRepository.kt | 20 +++++ features/manage-tokens/impl/build.gradle.kts | 3 + 14 files changed, 302 insertions(+), 15 deletions(-) create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsPagingSource.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensListRepository.kt create mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetGlobalTokenListUseCase.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensListRepository.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index d66c81c65f..d4488fe919 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -2,10 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -231,4 +228,10 @@ internal object TokensDomainModule { ): GetMissedAddressesCryptoCurrenciesUseCase { return GetMissedAddressesCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository) } + + @Provides + @ViewModelScoped + fun provideGetGlobalTokenListUseCase(tokensListRepository: TokensListRepository): GetGlobalTokenListUseCase { + return GetGlobalTokenListUseCase(repository = tokensListRepository) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt index cf0b07bbfe..277d3f4a52 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt @@ -32,6 +32,16 @@ fun CryptoCurrency.Token.tryGetBackgroundForTokenIcon( ): Color { if (isGrayscale) return TangemColorPalette.Dark2 + return tryGetBackgroundForTokenIcon(contractAddress = contractAddress, fallbackColor = fallbackColor) +} + +/** + * Tries to extract a background color from the contract address of a token. + * + * @param fallbackColor The color to use as a fallback. + * @return The extracted background color or the fallback color if extraction fails or if it is a test network token. + */ +fun tryGetBackgroundForTokenIcon(contractAddress: String, fallbackColor: Color = TangemColorPalette.Black): Color { return try { val colorHex = "#" + contractAddress.substring(range = COLOR_HEX_START_INDEX..COLOR_HEX_END_INDEX) Color(colorHex.toColorInt()) diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index 88d7471830..f183caa14f 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -45,4 +45,5 @@ dependencies { implementation(deps.jodatime) implementation(deps.timber) implementation(deps.retrofit) // For HttpException + implementation(deps.androidx.paging.runtime) } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 80ceefbdc9..f435378502 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -1,10 +1,7 @@ package com.tangem.data.tokens.di import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.tokens.repository.DefaultCurrenciesRepository -import com.tangem.data.tokens.repository.DefaultMarketCryptoCurrencyRepository -import com.tangem.data.tokens.repository.DefaultNetworksRepository -import com.tangem.data.tokens.repository.DefaultQuotesRepository +import com.tangem.data.tokens.repository.* import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -12,10 +9,7 @@ import com.tangem.datasource.local.quote.QuotesStore import com.tangem.datasource.local.token.UserMarketCoinsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -93,4 +87,18 @@ internal object TokensDataModule { ): MarketCryptoCurrencyRepository { return DefaultMarketCryptoCurrencyRepository(userMarketCoinsStore) } + + @Provides + @Singleton + fun providesTokensListRepository( + tangemTechApi: TangemTechApi, + dispatchers: CoroutineDispatcherProvider, + quotesRepository: QuotesRepository, + ): TokensListRepository { + return DefaultTokensListRepository( + tangemTechApi = tangemTechApi, + dispatchers = dispatchers, + quotesRepository = quotesRepository, + ) + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsPagingSource.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsPagingSource.kt new file mode 100644 index 0000000000..beb9f10b36 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsPagingSource.kt @@ -0,0 +1,77 @@ +package com.tangem.data.tokens.paging + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.CoinsResponse +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider + +/** + * A PagingSource responsible for retrieving tokens from the Tangem Tech API and adding to them quotes information. + * + * @property api Tangem Tech API + * @property quotesRepository Repository providing quotes data. + * @property dispatchers Coroutine dispatchers provider. + * @property searchText The search text used to filter tokens. + */ +internal class CoinsPagingSource( + private val api: TangemTechApi, + private val quotesRepository: QuotesRepository, + private val dispatchers: CoroutineDispatcherProvider, + private val searchText: String?, +) : PagingSource() { + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(other = 1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(other = 1) + } + } + + override suspend fun load(params: LoadParams): LoadResult { + val page = params.key ?: 0 + + return com.tangem.utils.coroutines.runCatching(dispatchers.io) { + api.getCoins( + active = true, // TODO change when voting functionality is implemented + searchText = searchText, + offset = page * params.loadSize, + limit = params.loadSize, + ) + }.fold( + onSuccess = { response -> + val coinsIds = response.coins.map { coin -> + CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkId(coin.id), + suffix = CryptoCurrency.ID.Suffix.RawID(coin.id), + ) + } + val quotes = quotesRepository.getQuotesSync(currenciesIds = coinsIds.toSet(), refresh = false) + + LoadResult.Page( + data = CoinsResponseConverter.convert( + CoinsData( + response.coins, + response.imageHost, + quotes, + ), + ), + prevKey = if (page == 0) null else page.minus(other = 1), + nextKey = if (response.coins.isEmpty()) null else page.plus(other = 1), + ) + }, + onFailure = { LoadResult.Error(it) }, + ) + } +} + +data class CoinsData( + val coins: List, + val imageHost: String?, + val quotes: Set, +) \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt new file mode 100644 index 0000000000..7735b24528 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt @@ -0,0 +1,51 @@ +package com.tangem.data.tokens.paging + +import com.tangem.blockchain.common.Blockchain +import com.tangem.data.tokens.utils.getNetworkStandardType +import com.tangem.datasource.api.tangemTech.models.CoinsResponse +import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Token +import com.tangem.utils.converter.Converter + +/** + * Converter from data model [CoinsResponse] to list of domain models [Token] + */ +internal object CoinsResponseConverter : Converter> { + + override fun convert(value: CoinsData): List { + return value.coins.map { coin -> + val id = CryptoCurrency.ID( + CryptoCurrency.ID.Prefix.TOKEN_PREFIX, + CryptoCurrency.ID.Body.NetworkId(coin.id), + CryptoCurrency.ID.Suffix.RawID(coin.id), + ) + val quote = value.quotes.find { it.rawCurrencyId == id.rawCurrencyId } + Token( + id = id.rawCurrencyId ?: coin.name, + name = coin.name, + symbol = coin.symbol, + iconUrl = getIconUrl(coin.id, value.imageHost), + isAvailable = coin.active, + networks = coin.networks.mapNotNull { network -> + val blockchain = Blockchain.fromNetworkId(network.networkId) ?: return@mapNotNull null + Token.Network( + networkId = network.networkId, + standardType = getNetworkStandardType(blockchain).name, + address = network.contractAddress, + iconUrl = getIconUrl(network.networkId, value.imageHost), + decimalCount = network.decimalCount?.toInt(), + ) + }, + quote = quote, + ) + } + } +} + +internal fun getIconUrl(id: String, imageHost: String? = null): String { + return "${imageHost ?: DEFAULT_IMAGE_HOST}large/$id.png" +} + +private const val DEFAULT_IMAGE_HOST = + "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/" \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensListRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensListRepository.kt new file mode 100644 index 0000000000..d4f2d9a9a2 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensListRepository.kt @@ -0,0 +1,45 @@ +package com.tangem.data.tokens.repository + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.tangem.data.tokens.paging.CoinsPagingSource +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.repository.TokensListRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow + +/** + * Default repository implementation for managing operations related to a complete set of tokens + * + * @property tangemTechApi Tangem Tech API + * @property dispatchers coroutine dispatchers provider + * @property quotesRepository responsible for providing cryptocurrency quotes data + * + */ +internal class DefaultTokensListRepository( + private val tangemTechApi: TangemTechApi, + private val dispatchers: CoroutineDispatcherProvider, + private val quotesRepository: QuotesRepository, +) : TokensListRepository { + + override fun getTokens(searchText: String?): Flow> { + return Pager( + config = PagingConfig( + pageSize = 100, + prefetchDistance = 70, + enablePlaceholders = false, + ), + pagingSourceFactory = { + CoinsPagingSource( + api = tangemTechApi, + dispatchers = dispatchers, + searchText = searchText, + quotesRepository = quotesRepository, + ) + }, + ).flow + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt index 5e87555133..21ef628101 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt @@ -50,7 +50,7 @@ private fun getNetworkDerivationPath( } } -private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType { +internal fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType { return when (blockchain) { Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.StandardType.ERC20 Blockchain.BSC, Blockchain.BSCTestnet -> Network.StandardType.BEP20 diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index c146658684..8fabb669f0 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -22,6 +22,9 @@ dependencies { /** Project - Other */ implementation(projects.core.utils) + /** Android - Other */ + implementation(deps.androidx.paging.runtime) + /** Utils */ implementation(deps.jodatime) implementation(deps.reKotlin) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt index 899574d5e4..9f5dfbe387 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt @@ -3,14 +3,17 @@ package com.tangem.domain.tokens.model import java.math.BigDecimal /** - * Represents a financial quote for a specific cryptocurrency, including its fiat exchange rate and price change. + * Represents financial information for a specific cryptocurrency, including its fiat exchange rate and price change. * - * @property rawCurrencyId The unique identifier of the token for which the quote is provided. + * @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided. * @property fiatRate The current fiat exchange rate for the cryptocurrency. * @property priceChange The price change for the cryptocurrency. + * @property values The values representing the cryptocurrency's price changes over a 24-hour period, + * suitable for chart plotting. */ data class Quote( val rawCurrencyId: String, val fiatRate: BigDecimal, val priceChange: BigDecimal, + val values: List? = null, ) \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt new file mode 100644 index 0000000000..ed26e356b8 --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt @@ -0,0 +1,41 @@ +package com.tangem.domain.tokens.model + +/** + * Represents a domain model for a token (as part of a general list of tokens). + * + * @property id The unique identifier of the token. + * @property networks List of networks associated with the token. + * @property isAvailable Indicates whether this token is supported in Tangem app. + * @property quote Equivalent prices for the token. + * @property name The name of the token. + * @property symbol The brief name of the token, e.g., "BTC". + * @property iconUrl URL of the token's icon. + */ +data class Token( + val id: String, + val networks: List, + val isAvailable: Boolean, + val quote: Quote?, + val name: String, + val symbol: String, + val iconUrl: String, +) { + + /** + * Represents a domain model for a network associated with a token. + * + * @property networkId The unique identifier of the network. + * @property standardType The type of blockchain associated with the network. + * @property address The contract address of the token on the current network. + * It is null for the main currencies of the network. + * @property iconUrl URL of the network's icon. + * @property decimalCount The decimal count associated with the token on the network. + */ + data class Network( + val networkId: String, + val standardType: String, + val address: String?, + val iconUrl: String, + val decimalCount: Int?, + ) +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetGlobalTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetGlobalTokenListUseCase.kt new file mode 100644 index 0000000000..74ddd5b4e4 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetGlobalTokenListUseCase.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.tokens + +import androidx.paging.PagingData +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.repository.TokensListRepository +import kotlinx.coroutines.flow.Flow + +/** + * Use case for retrieving a complete set of tokens with their quotes (filtered by a search text if provided). + * + * @property repository The repository responsible for fetching and preparing the list of tokens with their quotes. + */ +class GetGlobalTokenListUseCase(private val repository: TokensListRepository) { + + /** + * @param searchText The text used for filtering the list of tokens (not required). + * @return A [Flow] emitting [PagingData] containing the tokens with their quotes. + */ + operator fun invoke(searchText: String?): Flow> { + return repository.getTokens(searchText = searchText?.ifBlank(defaultValue = { null })) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensListRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensListRepository.kt new file mode 100644 index 0000000000..96db81b6af --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensListRepository.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.tokens.repository + +import androidx.paging.PagingData +import com.tangem.domain.tokens.model.Token +import kotlinx.coroutines.flow.Flow + +/** + * Repository interface for managing operations related to a complete set of tokens + * (not associated with any specific card). + * */ +interface TokensListRepository { + + /** + * Retrieves a list of available tokens with quotes, can be filtered based on the provided search text. + * + * @param searchText The search text used to filter tokens. + * @return A [Flow] emitting [PagingData] containing the tokens with quotes matching the search criteria. + */ + fun getTokens(searchText: String?): Flow> +} \ No newline at end of file diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 76bd921f71..fab77971a3 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -47,6 +47,9 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.utils) + /** Project - Data */ + implementation(projects.data.tokens) + /** Domain modules */ implementation(projects.common) implementation(projects.domain.card) From 95510a665c03ecc484d511d0bf27340af5fc853b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 30 Nov 2023 19:09:13 +0300 Subject: [PATCH 18/20] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../feature/wallet/presentation/router/DefaultWalletRouter.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 2a612cd92b..6ef8ce45d1 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 2a612cd92b1c78b99c917d0fe66342831f801e5e +Subproject commit 6ef8ce45d183905b5752e2d33c1d8bf2f4bcace6 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 e5ad37394c..8807297f9f 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 @@ -63,6 +63,7 @@ internal class DefaultWalletRouter( val viewModel = hiltViewModel().apply { router = this@DefaultWalletRouter } + LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) WalletScreen(state = viewModel.uiState) } From 34ffa9a1e719612fe0e7fac58bea264d316abde1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 1 Dec 2023 19:40:54 +0400 Subject: [PATCH 19/20] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../java/com/tangem/tap/TapApplication.kt | 6 ++++ .../amplitude/AmplitudeAnalyticsHandler.kt | 4 +-- .../appsFlyer/AppsFlyerAnalyticsHandler.kt | 4 +-- .../firebase/FirebaseAnalyticsHandler.kt | 4 +-- core/analytics/build.gradle.kts | 9 +++--- .../core/analytics/models/AnalyticsEvent.kt | 5 ++- .../analytics/models/OneTimeAnalyticsEvent.kt | 6 ++++ core/analytics/src/main/AndroidManifest.xml | 2 -- .../core/analytics/api/EventFilterApi.kt | 2 +- .../core/analytics/api/EventHandlerApi.kt | 6 ++-- .../core/analytics/di/AnalyticsModule.kt | 9 +++++- .../analytics/filter/OneTimeEventFilter.kt | 30 +++++++++++++++++ .../repository/AnalyticsRepository.kt | 8 +++++ .../local/preferences/PreferencesKeys.kt | 2 ++ data/analytics/.gitignore | 1 + data/analytics/build.gradle.kts | 32 +++++++++++++++++++ .../analytics/DefaultAnalyticsRepository.kt | 27 ++++++++++++++++ .../data/analytics/di/AnalyticsDataModule.kt | 19 +++++++++++ settings.gradle.kts | 2 +- 20 files changed, 158 insertions(+), 21 deletions(-) create mode 100644 core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimeAnalyticsEvent.kt delete mode 100644 core/analytics/src/main/AndroidManifest.xml create mode 100644 core/analytics/src/main/java/com/tangem/core/analytics/filter/OneTimeEventFilter.kt create mode 100644 core/analytics/src/main/java/com/tangem/core/analytics/repository/AnalyticsRepository.kt create mode 100644 data/analytics/.gitignore create mode 100644 data/analytics/build.gradle.kts create mode 100644 data/analytics/src/main/kotlin/com/tangem/data/analytics/DefaultAnalyticsRepository.kt create mode 100644 data/analytics/src/main/kotlin/com/tangem/data/analytics/di/AnalyticsDataModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e3f7a4fcde..a3a05911c7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -74,6 +74,7 @@ dependencies { implementation(projects.data.tokens) implementation(projects.data.txhistory) implementation(projects.data.wallets) + implementation(projects.data.analytics) /** Features */ implementation(projects.features.onboarding) diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 420a6ff091..0aecd2719a 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -14,6 +14,7 @@ import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.datasource.api.common.MoshiConverter @@ -216,6 +217,9 @@ internal class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var sendFeatureToggles: SendFeatureToggles + + @Inject + lateinit var oneTimeEventFilter: OneTimeEventFilter // endregion Injected override fun onCreate() { @@ -353,6 +357,8 @@ internal class TapApplication : Application(), ImageLoaderFactory { factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder()) factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder()) + factory.addFilter(oneTimeEventFilter) + val buildData = AnalyticsHandlerBuilder.Data( application = application, config = config, diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt index a1daeb7ccf..5eed564c97 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt @@ -9,8 +9,8 @@ class AmplitudeAnalyticsHandler( override fun id(): String = ID - override fun send(event: String, params: Map) { - client.logEvent(event, params) + override fun send(eventId: String, params: Map) { + client.logEvent(eventId, params) } companion object { diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt index c4b12fc278..710c7308e6 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt @@ -12,8 +12,8 @@ class AppsFlyerAnalyticsHandler( override fun id(): String = ID - override fun send(event: String, params: Map) { - client.logEvent(event, params) + override fun send(eventId: String, params: Map) { + client.logEvent(eventId, params) } override fun send(event: AnalyticsEvent) { diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt index e445597e8f..10b399382c 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt @@ -14,8 +14,8 @@ class FirebaseAnalyticsHandler( override fun id(): String = ID - override fun send(event: String, params: Map) { - client.logEvent(event, params) + override fun send(eventId: String, params: Map) { + client.logEvent(eventId, params) } override fun send(event: AnalyticsEvent) { diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts index 29b4cfb591..663fb0f33e 100644 --- a/core/analytics/build.gradle.kts +++ b/core/analytics/build.gradle.kts @@ -1,19 +1,18 @@ plugins { - alias(deps.plugins.android.library) - alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.jvm) alias(deps.plugins.kotlin.kapt) - alias(deps.plugins.hilt.android) id("configuration") } dependencies { /** DI */ - implementation(deps.hilt.android) + implementation(deps.hilt.core) kapt(deps.hilt.kapt) /** Core shouldn't depends on core, but in case with utils and logging its necessary */ implementation(projects.core.utils) - implementation(projects.core.analytics.models) + + implementation(deps.kotlin.coroutines) } \ No newline at end of file diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt index e60b14d1ee..025a6c1038 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt @@ -8,4 +8,7 @@ open class AnalyticsEvent( val event: String, var params: Map = mapOf(), val error: Throwable? = null, -) \ No newline at end of file +) { + + val id: String = "[$category] $event" +} \ No newline at end of file diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimeAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimeAnalyticsEvent.kt new file mode 100644 index 0000000000..82d9991c71 --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimeAnalyticsEvent.kt @@ -0,0 +1,6 @@ +package com.tangem.core.analytics.models + +interface OneTimeAnalyticsEvent { + + val oneTimeEventId: String +} \ No newline at end of file diff --git a/core/analytics/src/main/AndroidManifest.xml b/core/analytics/src/main/AndroidManifest.xml deleted file mode 100644 index 9270c6ee97..0000000000 --- a/core/analytics/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventFilterApi.kt b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventFilterApi.kt index 4de7e4aee8..dc94652208 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventFilterApi.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventFilterApi.kt @@ -16,7 +16,7 @@ interface AnalyticsEventFilter { * An internal filter check that, on external or internal conditions, recognizes the possibility of * sending an event. */ - fun canBeSent(event: AnalyticsEvent): Boolean + suspend fun canBeSent(event: AnalyticsEvent): Boolean /** * Performs a check to see if the event can be dispatched by a specific handler diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt index 0a5f3179de..7bedfebff3 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt @@ -12,13 +12,11 @@ interface AnalyticsEventHandler { interface AnalyticsHandler : AnalyticsEventHandler { fun id(): String - fun send(event: String, params: Map = emptyMap()) + fun send(eventId: String, params: Map = emptyMap()) override fun send(event: AnalyticsEvent) { - send(prepareEventString(event), event.params) + send(event.id, event.params) } - - fun prepareEventString(event: AnalyticsEvent): String = "[${event.category}] ${event.event}" } interface ErrorEventHandler { diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt b/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt index 3b12478c96..b4bb704732 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt @@ -2,6 +2,8 @@ package com.tangem.core.analytics.di import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.filter.OneTimeEventFilter +import com.tangem.core.analytics.repository.AnalyticsRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -10,11 +12,16 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -class AnalyticsModule { +internal object AnalyticsModule { @Singleton @Provides fun provideAnalyticsHandler(): AnalyticsEventHandler { return Analytics // todo replace after refactoring calling Analytics in whole project } + + @Provides + fun provideOneTimeEventFilter(analyticsRepository: AnalyticsRepository): OneTimeEventFilter { + return OneTimeEventFilter(analyticsRepository) + } } \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/filter/OneTimeEventFilter.kt b/core/analytics/src/main/java/com/tangem/core/analytics/filter/OneTimeEventFilter.kt new file mode 100644 index 0000000000..c224eff75b --- /dev/null +++ b/core/analytics/src/main/java/com/tangem/core/analytics/filter/OneTimeEventFilter.kt @@ -0,0 +1,30 @@ +package com.tangem.core.analytics.filter + +import com.tangem.core.analytics.api.AnalyticsEventFilter +import com.tangem.core.analytics.api.AnalyticsHandler +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.OneTimeAnalyticsEvent +import com.tangem.core.analytics.repository.AnalyticsRepository + +class OneTimeEventFilter( + private val analyticsRepository: AnalyticsRepository, +) : AnalyticsEventFilter { + + override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = event is OneTimeAnalyticsEvent + + override suspend fun canBeSent(event: AnalyticsEvent): Boolean { + if (event !is OneTimeAnalyticsEvent) return true + + val isSent = analyticsRepository.checkIsEventSent(event.oneTimeEventId) + + if (!isSent) { + analyticsRepository.setIsEventSent(event.oneTimeEventId) + } + + return !isSent + } + + override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean { + return canBeAppliedTo(event) + } +} \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/repository/AnalyticsRepository.kt b/core/analytics/src/main/java/com/tangem/core/analytics/repository/AnalyticsRepository.kt new file mode 100644 index 0000000000..859220527a --- /dev/null +++ b/core/analytics/src/main/java/com/tangem/core/analytics/repository/AnalyticsRepository.kt @@ -0,0 +1,8 @@ +package com.tangem.core.analytics.repository + +interface AnalyticsRepository { + + suspend fun checkIsEventSent(eventId: String): Boolean + + suspend fun setIsEventSent(eventId: String) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 6eda0886c9..0ad9c10237 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -34,6 +34,8 @@ object PreferencesKeys { val BALANCE_HIDING_SETTINGS_KEY by lazy { stringPreferencesKey(name = "balanceHidingSettings") } val WALLETS_SCROLL_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "walletsScrollPreview") } + + val SENT_ONE_TIME_EVENTS_KEY by lazy { stringPreferencesKey(name = "sentOneTimeEvents") } } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/data/analytics/.gitignore b/data/analytics/.gitignore new file mode 100644 index 0000000000..3a48006eea --- /dev/null +++ b/data/analytics/.gitignore @@ -0,0 +1 @@ +/buld diff --git a/data/analytics/build.gradle.kts b/data/analytics/build.gradle.kts new file mode 100644 index 0000000000..258c49ea37 --- /dev/null +++ b/data/analytics/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.analytics" +} + +dependencies { + + /** Project - Analytics */ + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + + /** Project - Data */ + implementation(projects.core.datasource) + implementation(projects.data.common) + + /** AndroidX */ + implementation(deps.androidx.datastore) + + /** DI */ + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) + + /** Other */ + implementation(deps.kotlin.coroutines) + implementation(deps.timber) +} \ No newline at end of file diff --git a/data/analytics/src/main/kotlin/com/tangem/data/analytics/DefaultAnalyticsRepository.kt b/data/analytics/src/main/kotlin/com/tangem/data/analytics/DefaultAnalyticsRepository.kt new file mode 100644 index 0000000000..9235045bae --- /dev/null +++ b/data/analytics/src/main/kotlin/com/tangem/data/analytics/DefaultAnalyticsRepository.kt @@ -0,0 +1,27 @@ +package com.tangem.data.analytics + +import com.tangem.core.analytics.repository.AnalyticsRepository +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectListSync + +internal class DefaultAnalyticsRepository( + private val appPreferencesStore: AppPreferencesStore, +) : AnalyticsRepository { + + override suspend fun checkIsEventSent(eventId: String): Boolean { + val sentEvents = appPreferencesStore + .getObjectListSync(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY) + + return eventId in sentEvents + } + + override suspend fun setIsEventSent(eventId: String) { + appPreferencesStore.editData { mutablePreferences -> + val sentEvents = mutablePreferences.getObject>(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY) + val updatedSentEvents = sentEvents.orEmpty() + eventId + + mutablePreferences.setObject(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY, updatedSentEvents) + } + } +} \ No newline at end of file diff --git a/data/analytics/src/main/kotlin/com/tangem/data/analytics/di/AnalyticsDataModule.kt b/data/analytics/src/main/kotlin/com/tangem/data/analytics/di/AnalyticsDataModule.kt new file mode 100644 index 0000000000..ed3e2b04ac --- /dev/null +++ b/data/analytics/src/main/kotlin/com/tangem/data/analytics/di/AnalyticsDataModule.kt @@ -0,0 +1,19 @@ +package com.tangem.data.analytics.di + +import com.tangem.core.analytics.repository.AnalyticsRepository +import com.tangem.data.analytics.DefaultAnalyticsRepository +import com.tangem.datasource.local.preferences.AppPreferencesStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal object AnalyticsDataModule { + + @Provides + fun provideAnalyticsRepository(appPreferencesStore: AppPreferencesStore): AnalyticsRepository { + return DefaultAnalyticsRepository(appPreferencesStore) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 59ac1ba906..1abc2626ae 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -128,7 +128,6 @@ include(":domain:app-theme:models") include(":domain:balance-hiding") include(":domain:balance-hiding:models") include(":domain:transaction") - // endregion Domain modules // region Data modules @@ -142,4 +141,5 @@ include(":data:source:preferences") include(":data:settings") include(":data:txhistory") include(":data:wallets") +include(":data:analytics") // endregion Data modules \ No newline at end of file From cad55c940886b7502f5f1f93e57758612959e5db Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Dec 2023 13:12:53 +0400 Subject: [PATCH 20/20] Updated on 2026-08-14 --- .../BiometricUserWalletsListManager.kt | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt index af472b3608..c285a15c80 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt @@ -67,15 +67,17 @@ internal class BiometricUserWalletsListManager( } } .map { - if (throwIfNotAllWalletsUnlocked && state.value.userWallets.any(UserWallet::isLocked)) { - Timber.e("Not all user wallets have been unlocked") + val userWallets = state.value.userWallets + + if (throwIfNotAllWalletsUnlocked && userWallets.any(UserWallet::isLocked)) { + Timber.e("Some user wallets remain locked") throw UserWalletsListError.NotAllUserWalletsUnlocked } val selectedUserWallet = selectedUserWalletSync if (selectedUserWallet == null || selectedUserWallet.isLocked) { - Timber.e("Unable to find selected user wallet") - throw UserWalletsListError.NoUserWalletSelected + findAndSetUnlockedUserWallet(userWallets) + ?: throw UserWalletsListError.NoUserWalletSelected } else { selectedUserWallet } @@ -280,13 +282,14 @@ internal class BiometricUserWalletsListManager( prevSelectedWalletId: UserWalletId?, userWallets: List, ): UserWalletId? { - val findUnlockedAndSet = { - userWallets.firstOrNull { !it.isLocked } - ?.walletId - ?.also { selectedUserWalletRepository.set(it) } - } + return prevSelectedWalletId + ?: (selectedUserWalletRepository.get() ?: findAndSetUnlockedUserWallet(userWallets)?.walletId) + } - return prevSelectedWalletId ?: (selectedUserWalletRepository.get() ?: findUnlockedAndSet()) + private fun findAndSetUnlockedUserWallet(userWallets: List): UserWallet? { + return userWallets + .firstOrNull { !it.isLocked } + ?.also { selectedUserWalletRepository.set(it.walletId) } } private fun changeSelectedUserWalletIdIfNeeded(walletsIdsToRemove: List) {