Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-18 11:28:18 +01:00
commit 226a413558
994 changed files with 27178 additions and 11929 deletions

View file

@ -1,20 +0,0 @@
package com.tangem.feature.wallet.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object FeatureTogglesModule {
@Provides
@Singleton
fun provideWalletFeatureToggles(featureTogglesManager: FeatureTogglesManager): WalletFeatureToggles {
return WalletFeatureToggles(featureTogglesManager)
}
}

View file

@ -1,6 +1,8 @@
package com.tangem.feature.wallet.di
import com.tangem.core.navigation.ReduxNavController
import com.tangem.common.routing.AppRouter
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.feature.wallet.presentation.router.DefaultWalletRouter
import com.tangem.features.wallet.navigation.WalletRouter
import dagger.Module
@ -15,7 +17,11 @@ internal object WalletRouterModule {
@Provides
@ActivityScoped
fun provideWalletRouter(reduxNavController: ReduxNavController): WalletRouter {
return DefaultWalletRouter(reduxNavController = reduxNavController)
fun provideWalletRouter(
appRouter: AppRouter,
urlOpener: UrlOpener,
reduxStateHolder: ReduxStateHolder,
): WalletRouter {
return DefaultWalletRouter(appRouter, urlOpener, reduxStateHolder)
}
}

View file

@ -1,11 +0,0 @@
package com.tangem.feature.wallet.featuretoggle
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
internal class WalletFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) {
val isTokenListLceFlowEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("TOKEN_LIST_LCE_ENABLED")
}

View file

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

View file

@ -1,7 +1,7 @@
package com.tangem.feature.wallet.presentation.common
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
@ -67,7 +67,7 @@ internal object WalletPreviewData {
}
val coinIconState
get() = TokenIconState.CoinIcon(
get() = CurrencyIconState.CoinIcon(
url = null,
fallbackResId = R.drawable.img_polygon_22,
isGrayscale = false,
@ -75,9 +75,9 @@ internal object WalletPreviewData {
)
private val tokenIconState
get() = TokenIconState.TokenIcon(
get() = CurrencyIconState.TokenIcon(
url = null,
networkBadgeIconResId = R.drawable.img_polygon_22,
topBadgeIconResId = R.drawable.img_polygon_22,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
@ -85,10 +85,10 @@ internal object WalletPreviewData {
)
private val customTokenIconState
get() = TokenIconState.CustomTokenIcon(
get() = CurrencyIconState.CustomTokenIcon(
tint = TangemColorPalette.Black,
background = TangemColorPalette.Meadow,
networkBadgeIconResId = R.drawable.img_polygon_22,
topBadgeIconResId = R.drawable.img_polygon_22,
isGrayscale = false,
)

View file

@ -16,7 +16,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Constraints
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.res.TangemTheme
@ -49,7 +49,7 @@ internal fun TokenItem(
.tokenClickable(state = state)
.background(color = TangemTheme.colors.background.primary),
) {
TokenIcon(
CurrencyIcon(
state = state.iconState,
modifier = Modifier
.layoutId(layoutId = LayoutId.ICON)

View file

@ -8,10 +8,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.common.Strings
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.utils.StringsSigns
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.CryptoAmountState as TokenCryptoAmountState
@Composable
@ -23,7 +23,7 @@ internal fun TokenCryptoAmount(
when (state) {
is TokenCryptoAmountState.Content -> {
CryptoAmountText(
amount = if (isBalanceHidden) Strings.STARS else state.text,
amount = if (isBalanceHidden) StringsSigns.STARS else state.text,
modifier = modifier,
)
}

View file

@ -7,9 +7,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.common.Strings
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.utils.StringsSigns
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.FiatAmountState as TokenFiatAmountState
@Composable
@ -17,7 +17,7 @@ internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Bool
when (state) {
is TokenFiatAmountState.Content -> {
FiatAmountText(
text = if (isBalanceHidden) Strings.STARS else state.text,
text = if (isBalanceHidden) StringsSigns.STARS else state.text,
modifier,
)
}

View file

@ -18,7 +18,7 @@ import com.tangem.core.ui.components.SpacerW6
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.CryptoPriceState as TokenPriceChangeState
@Composable
@ -33,7 +33,7 @@ internal fun TokenPrice(state: TokenPriceChangeState?, modifier: Modifier = Modi
)
}
is TokenPriceChangeState.Unknown -> {
PriceText(text = TokenItemState.UNKNOWN_AMOUNT_SIGN, modifier = modifier)
PriceText(text = DASH_SIGN, modifier = modifier)
}
is TokenPriceChangeState.Loading -> {
RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
@ -104,7 +104,7 @@ private fun PriceChangeIcon(type: PriceChangeType) {
private fun PriceChangeText(type: PriceChangeType?, text: String?, modifier: Modifier = Modifier) {
AnimatedContent(targetState = text, modifier = modifier, label = "Update the price change's text") { animatedText ->
Text(
text = animatedText ?: TokenItemState.UNKNOWN_AMOUNT_SIGN,
text = animatedText ?: DASH_SIGN,
color = when (type) {
PriceChangeType.UP -> TangemTheme.colors.text.accent
PriceChangeType.DOWN -> TangemTheme.colors.text.warning

View file

@ -1,7 +1,6 @@
package com.tangem.feature.wallet.presentation.common.preview
import androidx.compose.runtime.mutableStateOf
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
@ -12,13 +11,12 @@ import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarConfig
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.features.managetokens.navigation.ExpandableState
import kotlinx.collections.immutable.persistentListOf
internal object WalletScreenPreviewData {
private val tokenItemState = TokenItemState.Content(
id = "1",
iconState = TokenIconState.Locked,
iconState = CurrencyIconState.Locked,
titleState = TokenItemState.TitleState.Content(text = "Bitcoin"),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "0,35853044 BTC"),
@ -58,7 +56,7 @@ internal object WalletScreenPreviewData {
WalletTokensListState.TokensListItemState.Token(
state = TokenItemState.Unreachable(
id = "3",
iconState = TokenIconState.Locked,
iconState = CurrencyIconState.Locked,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
onItemClick = {},
onItemLongClick = {},
@ -151,7 +149,6 @@ internal object WalletScreenPreviewData {
internal val walletScreenState = WalletScreenState(
onBackClick = {},
manageTokensExpandableState = mutableStateOf(ExpandableState.COLLAPSED),
topBarConfig = topBarConfig,
selectedWalletIndex = 0,
wallets = persistentListOf(
@ -161,6 +158,5 @@ internal object WalletScreenPreviewData {
onWalletChange = {},
event = consumedEvent(),
isHidingMode = false,
manageTokenRedesignToggle = false,
)
}

View file

@ -1,7 +1,7 @@
package com.tangem.feature.wallet.presentation.common.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
/** Token item state */
@ -10,7 +10,7 @@ internal sealed class TokenItemState {
abstract val id: String
abstract val iconState: TokenIconState
abstract val iconState: CurrencyIconState
abstract val titleState: TitleState
@ -23,7 +23,7 @@ internal sealed class TokenItemState {
/** Loading token state */
data class Loading(
override val id: String,
override val iconState: TokenIconState,
override val iconState: CurrencyIconState,
override val titleState: TitleState.Content,
) : TokenItemState() {
override val fiatAmountState: FiatAmountState = FiatAmountState.Loading
@ -33,7 +33,7 @@ internal sealed class TokenItemState {
/** Locked token state */
data class Locked(override val id: String) : TokenItemState() {
override val iconState: TokenIconState = TokenIconState.Locked
override val iconState: CurrencyIconState = CurrencyIconState.Locked
override val titleState: TitleState = TitleState.Locked
override val fiatAmountState: FiatAmountState = FiatAmountState.Locked
override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Locked
@ -51,7 +51,7 @@ internal sealed class TokenItemState {
*/
data class Content(
override val id: String,
override val iconState: TokenIconState,
override val iconState: CurrencyIconState,
override val titleState: TitleState,
override val fiatAmountState: FiatAmountState,
override val cryptoAmountState: CryptoAmountState.Content,
@ -69,7 +69,7 @@ internal sealed class TokenItemState {
*/
data class Draggable(
override val id: String,
override val iconState: TokenIconState,
override val iconState: CurrencyIconState,
override val titleState: TitleState,
override val cryptoAmountState: CryptoAmountState,
) : TokenItemState() {
@ -88,7 +88,7 @@ internal sealed class TokenItemState {
*/
data class Unreachable(
override val id: String,
override val iconState: TokenIconState,
override val iconState: CurrencyIconState,
override val titleState: TitleState,
val onItemClick: () -> Unit,
val onItemLongClick: () -> Unit,
@ -108,7 +108,7 @@ internal sealed class TokenItemState {
*/
data class NoAddress(
override val id: String,
override val iconState: TokenIconState,
override val iconState: CurrencyIconState,
override val titleState: TitleState,
val onItemLongClick: () -> Unit,
) : TokenItemState() {
@ -161,8 +161,4 @@ internal sealed class TokenItemState {
object Locked : CryptoPriceState()
}
companion object {
const val UNKNOWN_AMOUNT_SIGN = ""
}
}

View file

@ -16,11 +16,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@ -28,6 +27,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
@ -36,6 +36,7 @@ import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkGroupItem
@ -57,8 +58,12 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier
Scaffold(
modifier = modifier,
topBar = {
TopBar(state.header, tokensListState)
TopBar(
config = state.header,
tokensListState = tokensListState,
)
},
contentWindowInsets = WindowInsetsZero,
content = { paddingValues ->
TokenList(
modifier = Modifier
@ -72,7 +77,9 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier
},
floatingActionButtonPosition = FabPosition.Center,
floatingActionButton = {
Actions(state.actions)
Box(modifier = Modifier.navigationBarsPadding()) {
Actions(state.actions)
}
},
containerColor = TangemTheme.colors.background.secondary,
)
@ -104,9 +111,11 @@ private fun TokenList(
onDragEnd = onDragEnd,
)
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val listContentPadding = PaddingValues(
top = TangemTheme.dimens.spacing4,
bottom = TangemTheme.dimens.spacing92,
bottom = TangemTheme.dimens.spacing92 + bottomBarHeight,
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
)
@ -140,7 +149,7 @@ private fun TokenList(
}
}
BottomGradient(modifier = Modifier.align(Alignment.BottomCenter))
BottomFade(modifier = Modifier.align(Alignment.BottomCenter))
}
}
@ -191,23 +200,6 @@ private fun LazyItemScope.DraggableItem(
}
}
@Composable
private fun BottomGradient(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size116)
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
TangemTheme.colors.background.secondary,
),
),
),
)
}
@Composable
private fun TopBar(
config: OrganizeTokensState.HeaderConfig,
@ -228,6 +220,7 @@ private fun TopBar(
modifier = modifier
.shadow(elevation)
.background(TangemTheme.colors.background.secondary)
.statusBarsPadding()
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
) {

View file

@ -13,9 +13,7 @@ import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.ToggleTokenListGroupingUseCase
import com.tangem.domain.tokens.ToggleTokenListSortingUseCase
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles
import com.tangem.feature.wallet.presentation.organizetokens.analytics.PortfolioOrganizeTokensAnalyticsEvent
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
@ -42,7 +40,6 @@ internal class OrganizeTokensViewModel @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val analyticsEventsHandler: AnalyticsEventHandler,
private val walletFeatureToggles: WalletFeatureToggles,
private val dispatchers: CoroutineDispatcherProvider,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, OrganizeTokensIntents {
@ -173,34 +170,21 @@ internal class OrganizeTokensViewModel @Inject constructor(
}
private suspend fun getTokenList(): TokenList? {
return if (walletFeatureToggles.isTokenListLceFlowEnabled) {
val tokenList = getTokenListUseCase.launchLce(userWalletId)
.transform { maybeTokenList ->
val tokenList = maybeTokenList.getOrElse(
ifLoading = { return@transform },
ifError = { error ->
stateHolder.updateStateWithError(error)
val tokenList = getTokenListUseCase.launch(userWalletId)
.transform { maybeTokenList ->
val tokenList = maybeTokenList.getOrElse(
ifLoading = { return@transform },
ifError = { error ->
stateHolder.updateStateWithError(error)
return@transform
},
)
return@transform
},
)
emit(tokenList)
}
tokenList.firstOrNull()
} else {
val maybeTokenList = getTokenListUseCase.launch(userWalletId)
.first { maybeTokenList ->
maybeTokenList.getOrNull()?.totalFiatBalance !is TotalFiatBalance.Loading
}
maybeTokenList.getOrElse { error ->
stateHolder.updateStateWithError(error)
null
emit(tokenList)
}
}
return tokenList.firstOrNull()
}
private fun bootstrapDragAndDropUpdates() {

View file

@ -1,6 +1,6 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus

View file

@ -1,12 +1,9 @@
package com.tangem.feature.wallet.presentation.router
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -16,27 +13,26 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.ReduxNavController
import com.tangem.core.navigation.StateDialog
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.redux.StateDialog
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.onboarding.navigation.OnboardingRouter
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.viewmodels.WalletViewModel
import com.tangem.features.details.DetailsEntryPoint
import com.tangem.features.managetokens.navigation.ExpandableState
import com.tangem.features.managetokens.navigation.ManageTokensUi
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import com.tangem.features.markets.component.MarketsListComponent
import kotlin.properties.Delegates
/** Default implementation of wallet feature router */
internal class DefaultWalletRouter(
private val reduxNavController: ReduxNavController,
private val router: AppRouter,
private val urlOpener: UrlOpener,
private val reduxStateHolder: ReduxStateHolder,
) : InnerWalletRouter {
private var navController: NavHostController by Delegates.notNull()
@ -45,7 +41,7 @@ internal class DefaultWalletRouter(
override fun getEntryFragment(): Fragment = WalletFragment.create()
@Composable
override fun Initialize(onFinish: () -> Unit, manageTokensUi: ManageTokensUi) {
override fun Initialize(onFinish: () -> Unit, marketsListComponent: MarketsListComponent?) {
this.onFinish = onFinish
NavHost(
@ -58,20 +54,9 @@ internal class DefaultWalletRouter(
subscribeToLifecycle(LocalLifecycleOwner.current)
}
var bottomSheetHeaderHeight by remember { mutableStateOf(0.dp) }
WalletScreen(
state = viewModel.uiState.collectAsStateWithLifecycle().value,
bottomSheetHeaderHeightProvider = { bottomSheetHeaderHeight },
bottomSheetContent = {
val state = remember { mutableStateOf(ExpandableState.COLLAPSED) }
// Manage Tokens
manageTokensUi.Content(
onHeaderSizeChange = { bottomSheetHeaderHeight = it },
state = state,
)
viewModel.setExpandableState(state)
},
marketsListComponent = marketsListComponent,
)
}
@ -87,7 +72,6 @@ internal class DefaultWalletRouter(
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
OrganizeTokensScreen(
modifier = Modifier.statusBarsPadding(),
state = uiState,
)
}
@ -95,7 +79,7 @@ internal class DefaultWalletRouter(
}
@SuppressLint("RestrictedApi")
override fun popBackStack(screen: AppScreen?) {
override fun popBackStack() {
/*
* It's hack that avoid issue with closing the wallet screen.
* We are using NavGraph only inside feature so first backstack's element is entry of NavGraph and
@ -103,11 +87,7 @@ internal class DefaultWalletRouter(
* If backstack contains only NavGraph entry and wallet screen entry then we close the wallet fragment.
*/
if (navController.currentBackStack.value.size == BACKSTACK_ENTRY_COUNT_TO_CLOSE_WALLET_SCREEN) {
if (screen != null) {
reduxNavController.navigate(action = NavigationAction.PopBackTo(screen))
} else {
onFinish.invoke()
}
onFinish.invoke()
} else {
navController.popBackStack()
}
@ -118,64 +98,53 @@ internal class DefaultWalletRouter(
}
override fun openDetailsScreen(selectedWalletId: UserWalletId) {
reduxNavController.navigate(
action = NavigationAction.NavigateTo(
screen = AppScreen.Details,
bundle = bundleOf(
DetailsEntryPoint.USER_WALLET_ID_KEY to selectedWalletId.stringValue,
),
router.push(
AppRoute.Details(
userWalletId = selectedWalletId,
),
)
}
override fun openOnboardingScreen() {
reduxNavController.navigate(
action = NavigationAction.NavigateTo(
screen = AppScreen.OnboardingWallet,
bundle = bundleOf(OnboardingRouter.CAN_SKIP_BACKUP to false),
),
router.push(
AppRoute.OnboardingWallet(canSkipBackup = false),
)
}
override fun openUrl(url: String) {
reduxNavController.navigate(action = NavigationAction.OpenUrl(url))
urlOpener.openUrl(url)
}
override fun openTokenDetails(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) {
val networkAddress = currencyStatus.value.networkAddress
if (networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty()) {
reduxNavController.navigate(
action = NavigationAction.NavigateTo(
screen = AppScreen.WalletDetails,
bundle = bundleOf(
TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId.stringValue,
TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currencyStatus.currency,
),
router.push(
AppRoute.CurrencyDetails(
userWalletId = userWalletId,
currency = currencyStatus.currency,
),
)
}
}
override fun openStoriesScreen() {
reduxNavController.navigate(action = NavigationAction.NavigateTo(screen = AppScreen.Home))
router.push(AppRoute.Home)
}
override fun openSaveUserWalletScreen() {
reduxNavController.navigate(action = NavigationAction.NavigateTo(AppScreen.SaveWallet))
router.push(AppRoute.SaveWallet)
}
override fun isWalletLastScreen(): Boolean = reduxNavController.getBackStack().lastOrNull() == AppScreen.Wallet
override fun isWalletLastScreen(): Boolean {
return router.stack.lastOrNull() is AppRoute.Wallet
}
override fun openManageTokensScreen() {
reduxNavController.navigate(action = NavigationAction.NavigateTo(AppScreen.ManageTokens))
router.push(AppRoute.ManageTokens)
}
override fun openScanFailedDialog() {
reduxNavController.navigate(
action = NavigationAction.OpenDialog(
StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN),
),
)
override fun openScanFailedDialog(onTryAgain: () -> Unit) {
reduxStateHolder.dispatchDialogShow(StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN, onTryAgain))
}
private companion object {

View file

@ -2,10 +2,9 @@ package com.tangem.feature.wallet.presentation.router
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import com.tangem.core.navigation.AppScreen
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.navigation.ManageTokensUi
import com.tangem.features.markets.component.MarketsListComponent
import com.tangem.features.wallet.navigation.WalletRouter
/**
@ -24,12 +23,11 @@ internal interface InnerWalletRouter : WalletRouter {
*
* @param onFinish finish activity callback
*/
@Suppress("TopLevelComposableFunctions")
@Composable
fun Initialize(onFinish: () -> Unit, manageTokensUi: ManageTokensUi)
fun Initialize(onFinish: () -> Unit, marketsListComponent: MarketsListComponent?)
/** Pop back stack */
fun popBackStack(screen: AppScreen? = null)
fun popBackStack()
/** Open organize tokens screen */
fun openOrganizeTokensScreen(userWalletId: UserWalletId)
@ -59,5 +57,5 @@ internal interface InnerWalletRouter : WalletRouter {
fun openManageTokensScreen()
/** Open scan failed dialog */
fun openScanFailedDialog()
fun openScanFailedDialog(onTryAgain: () -> Unit)
}

View file

@ -1,8 +1,8 @@
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.core.lce.Lce
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.promo.PromoBanner
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
@ -23,7 +23,6 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
import kotlin.collections.count
@ -47,11 +46,11 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
val promoFlow = flow { emit(promoRepository.getOkxPromoBanner()) }
return combine(
flow = getTokenListUseCase.launch(userWallet.walletId).conflate(),
flow2 = isReadyToShowRateAppUseCase().conflate(),
flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(),
flow4 = shouldShowSwapPromoWalletUseCase().conflate(),
flow5 = promoFlow.conflate(),
flow = getTokenListUseCase.launch(userWallet.walletId),
flow2 = isReadyToShowRateAppUseCase(),
flow3 = isNeedToBackupUseCase(userWallet.walletId),
flow4 = shouldShowSwapPromoWalletUseCase(),
flow5 = promoFlow,
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowPromo, promoBanner ->
readyForRateAppNotification = true
@ -113,7 +112,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private fun MutableList<WalletNotification>.addInformationalNotifications(
cardTypesResolver: CardTypesResolver,
maybeTokenList: Either<TokenListError, TokenList>,
maybeTokenList: Lce<TokenListError, TokenList>,
clickIntents: WalletClickIntents,
) {
addIf(
@ -125,7 +124,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
}
private fun MutableList<WalletNotification>.addMissingAddressesNotification(
maybeTokenList: Either<TokenListError, TokenList>,
maybeTokenList: Lce<TokenListError, TokenList>,
clickIntents: WalletClickIntents,
) {
val currencies = maybeTokenList.getMissingAddressCurrencies()
@ -141,26 +140,23 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
private fun Either<TokenListError, TokenList>.getMissingAddressCurrencies(): List<CryptoCurrency> {
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()
}
private fun Lce<TokenListError, TokenList>.getMissingAddressCurrencies(): List<CryptoCurrency> {
val tokenList = getOrNull(isPartialContentAccepted = false) ?: return emptyList()
currencies
.filter { it.value is CryptoCurrencyStatus.MissedDerivation }
.map(CryptoCurrencyStatus::currency)
},
)
val currencies = when (tokenList) {
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies)
is TokenList.Ungrouped -> tokenList.currencies
is TokenList.Empty -> emptyList()
}
return currencies
.filter { it.value is CryptoCurrencyStatus.MissedDerivation }
.map(CryptoCurrencyStatus::currency)
}
private fun MutableList<WalletNotification>.addWarningNotifications(
cardTypesResolver: CardTypesResolver,
tokenList: Either<TokenListError, TokenList>,
tokenList: Lce<TokenListError, TokenList>,
isNeedToBackup: Boolean,
clickIntents: WalletClickIntents,
) {
@ -182,19 +178,16 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
private fun Either<TokenListError, TokenList>.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()
}
private fun Lce<TokenListError, TokenList>.hasUnreachableNetworks(): Boolean {
val tokenList = getOrNull(isPartialContentAccepted = false) ?: return false
currencies.any { it.value is CryptoCurrencyStatus.Unreachable }
},
)
val currencies = when (tokenList) {
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies)
is TokenList.Ungrouped -> tokenList.currencies
is TokenList.Empty -> emptyList()
}
return currencies.any { it.value is CryptoCurrencyStatus.Unreachable }
}
private fun MutableList<WalletNotification>.addRateTheAppNotification(

View file

@ -5,7 +5,6 @@ import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
@ -28,7 +27,6 @@ internal class MultiWalletContentLoader(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
private val walletFeatureToggles: WalletFeatureToggles,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) : WalletContentLoader(id = userWallet.walletId) {
@ -42,7 +40,6 @@ internal class MultiWalletContentLoader(
walletWithFundsChecker = walletWithFundsChecker,
getTokenListUseCase = getTokenListUseCase,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
walletFeatureToggles = walletFeatureToggles,
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
),

View file

@ -5,7 +5,6 @@ import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
@ -26,7 +25,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val walletFeatureToggles: WalletFeatureToggles,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) {
@ -42,7 +40,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
walletFeatureToggles = walletFeatureToggles,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
)
}

View file

@ -1,6 +1,5 @@
package com.tangem.feature.wallet.presentation.wallet.state
import androidx.compose.runtime.mutableStateOf
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.event.consumedEvent
import com.tangem.domain.wallets.models.UserWalletId
@ -11,8 +10,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTopBarCon
import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.WalletScreenStateTransformer
import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles
import com.tangem.features.managetokens.navigation.ExpandableState
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -27,9 +24,7 @@ import javax.inject.Singleton
[REDACTED_AUTHOR]
*/
@Singleton
internal class WalletStateController @Inject constructor(
private val manageTokensFeatureToggles: ManageTokensFeatureToggles,
) {
internal class WalletStateController @Inject constructor() {
val uiState: StateFlow<WalletScreenState> get() = mutableUiState
@ -89,8 +84,6 @@ internal class WalletStateController @Inject constructor(
onWalletChange = {},
event = consumedEvent(),
isHidingMode = false,
manageTokenRedesignToggle = manageTokensFeatureToggles.isRedesignedScreenEnabled,
manageTokensExpandableState = mutableStateOf(ExpandableState.EXPANDED),
)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.feature.wallet.presentation.wallet.state.model
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
data class PushNotificationsBottomSheetConfig(
val isFirstTimeAsking: Boolean,
val onRequest: () -> Unit,
val onAllow: () -> Unit,
val onDeny: () -> Unit,
val openSettings: () -> Unit,
) : TangemBottomSheetConfigContent

View file

@ -2,9 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.model
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.common.Strings
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.StringsSigns
import com.tangem.utils.StringsSigns.DASH_SIGN
/** Wallet card state */
@Immutable
@ -123,7 +124,7 @@ internal sealed interface WalletCardState {
}
companion object {
val HIDDEN_BALANCE_TEXT by lazy { TextReference.Str(value = Strings.STARS) }
val EMPTY_BALANCE_TEXT by lazy { TextReference.Str(value = "") }
val HIDDEN_BALANCE_TEXT by lazy { TextReference.Str(value = StringsSigns.STARS) }
val EMPTY_BALANCE_TEXT by lazy { TextReference.Str(value = DASH_SIGN) }
}
}

View file

@ -89,6 +89,19 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) {
),
)
data class Stake(
override val enabled: Boolean,
override val dimContent: Boolean,
override val onClick: () -> Unit,
) : WalletManageButton(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.common_stake),
iconResId = R.drawable.ic_arrow_down_24, // TODO staking
onClick = onClick,
dimContent = dimContent,
),
)
/**
* Sell
*

View file

@ -1,18 +1,14 @@
package com.tangem.feature.wallet.presentation.wallet.state.model
import androidx.compose.runtime.MutableState
import com.tangem.core.ui.event.StateEvent
import com.tangem.features.managetokens.navigation.ExpandableState
import kotlinx.collections.immutable.ImmutableList
internal data class WalletScreenState(
val onBackClick: () -> Unit,
val manageTokensExpandableState: MutableState<ExpandableState>,
val topBarConfig: WalletTopBarConfig,
val selectedWalletIndex: Int,
val wallets: ImmutableList<WalletState>,
val onWalletChange: (Int) -> Unit,
val event: StateEvent<WalletEvent>,
val isHidingMode: Boolean,
val manageTokenRedesignToggle: Boolean,
)

View file

@ -19,24 +19,25 @@ internal class SetBalancesAndLimitsTransformer(
private val userWallet: UserWallet,
private val maybeVisaCurrency: Either<Throwable, VisaCurrency>,
private val clickIntents: WalletClickIntents,
) : WalletStateTransformer(userWallet.walletId) {
) : TypedWalletStateTransformer<WalletState.Visa.Content>(
userWalletId = userWallet.walletId,
targetStateClass = WalletState.Visa.Content::class,
) {
override fun transform(prevState: WalletState): WalletState {
return prevState.transformWhenInState<WalletState.Visa.Content> { state ->
val visaCurrency = maybeVisaCurrency.getOrElse {
return state.copy(
walletCardState = getErrorWalletCardState(state.walletCardState),
depositButtonState = state.depositButtonState.copy(isEnabled = false),
balancesAndLimitBlockState = BalancesAndLimitsBlockState.Error,
)
}
state.copy(
walletCardState = getContentWalletCardState(state.walletCardState, visaCurrency),
depositButtonState = state.depositButtonState.copy(isEnabled = true),
balancesAndLimitBlockState = getContentBlockState(visaCurrency),
override fun transformTyped(prevState: WalletState.Visa.Content): WalletState {
val visaCurrency = maybeVisaCurrency.getOrElse {
return prevState.copy(
walletCardState = getErrorWalletCardState(prevState.walletCardState),
depositButtonState = prevState.depositButtonState.copy(isEnabled = false),
balancesAndLimitBlockState = BalancesAndLimitsBlockState.Error,
)
}
return prevState.copy(
walletCardState = getContentWalletCardState(prevState.walletCardState, visaCurrency),
depositButtonState = prevState.depositButtonState.copy(isEnabled = true),
balancesAndLimitBlockState = getContentBlockState(visaCurrency),
)
}
private fun getContentBlockState(visaCurrency: VisaCurrency) = BalancesAndLimitsBlockState.Content(

View file

@ -64,6 +64,7 @@ internal class SetRefreshStateTransformer(
is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled)
is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled)
is WalletManageButton.Receive -> button
is WalletManageButton.Stake -> null
is WalletManageButton.Swap -> null
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import kotlin.reflect.KClass
internal abstract class TypedWalletStateTransformer<S : WalletState>(
userWalletId: UserWalletId,
protected val targetStateClass: KClass<S>,
) : WalletStateTransformer(userWalletId) {
abstract fun transformTyped(prevState: S): WalletState
@Suppress("UNCHECKED_CAST")
final override fun transform(prevState: WalletState): WalletState {
return if (prevState::class == targetStateClass) {
transformTyped(prevState as S)
} else {
prevState
}
}
}

View file

@ -4,7 +4,6 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import kotlinx.collections.immutable.toImmutableList
import timber.log.Timber
internal abstract class WalletStateTransformer(
protected val userWalletId: UserWalletId,
@ -12,7 +11,7 @@ internal abstract class WalletStateTransformer(
abstract fun transform(prevState: WalletState): WalletState
override fun transform(prevState: WalletScreenState): WalletScreenState {
final override fun transform(prevState: WalletScreenState): WalletScreenState {
return prevState.copy(
wallets = prevState.wallets
.map { state ->
@ -21,13 +20,4 @@ internal abstract class WalletStateTransformer(
.toImmutableList(),
)
}
protected inline fun <reified S : WalletState> WalletState.transformWhenInState(
transform: (state: S) -> WalletState,
): WalletState = if (this is S) {
transform(this)
} else {
Timber.w("Impossible to transform ${this::class.simpleName} because current is ${S::class.simpleName}")
this
}
}

View file

@ -59,6 +59,11 @@ internal class MultiWalletCurrencyActionsConverter(
icon = R.drawable.ic_arrow_down_24
action = { clickIntents.onReceiveClick(cryptoCurrencyStatus) }
}
is TokenActionsState.ActionState.Stake -> {
title = resourceReference(R.string.common_stake)
icon = R.drawable.ic_arrow_down_24 // TODO staking replace icon
action = { clickIntents.onStakeClick(cryptoCurrencyStatus) }
}
is TokenActionsState.ActionState.Sell -> {
title = resourceReference(R.string.common_sell)
icon = R.drawable.ic_currency_24

View file

@ -1,15 +1,18 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.YieldBalance
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.WalletClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
internal class TokenItemStateConverter(
@ -61,13 +64,16 @@ internal class TokenItemStateConverter(
}
private fun CryptoCurrencyStatus.getFormattedAmount(): String {
val amount = value.amount ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN
val yieldBalance = (value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero()
val amount = value.amount?.plus(yieldBalance) ?: return DASH_SIGN
return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals)
}
private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String {
val fiatAmount = value.fiatAmount ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN
val yieldBalance = (value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero()
val fiatYieldBalance = value.fiatRate?.times(yieldBalance).orZero()
val fiatAmount = value.fiatAmount?.plus(fiatYieldBalance) ?: return DASH_SIGN
val appCurrency = appCurrencyProvider()
return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol)

View file

@ -9,6 +9,8 @@ import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import com.tangem.utils.StringsSigns.MINUS
import com.tangem.utils.StringsSigns.PLUS
import com.tangem.utils.converter.Converter
import com.tangem.utils.toBriefAddressFormat
import com.tangem.utils.toFormattedCurrencyString
@ -96,7 +98,7 @@ internal class TxHistoryItemStateConverter(
private fun TxHistoryItem.getAmount(): String {
val prefix = when (status) {
TxHistoryItem.TransactionStatus.Failed -> ""
else -> if (isOutgoing) "-" else "+"
else -> if (isOutgoing) MINUS else PLUS
}
return prefix + amount.toFormattedCurrencyString(currency = symbol, decimals = decimals)
}

View file

@ -9,6 +9,7 @@ import com.tangem.domain.visa.model.VisaCurrency
import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.VisaWalletIntents
import com.tangem.utils.StringsSigns
import com.tangem.utils.converter.Converter
import org.joda.time.DateTimeZone
@ -20,7 +21,7 @@ internal class VisaTxHistoryItemStateConverter(
override fun convert(value: VisaTxHistoryItem): TransactionState {
val localDate = value.date.withZone(DateTimeZone.getDefault())
val time = DateTimeFormatters.formatDate(localDate, DateTimeFormatters.timeFormatter)
val subtitle = "$time ${value.status.capitalize()}"
val subtitle = "$time ${StringsSigns.DOT} ${value.status.capitalize()}"
return TransactionState.Content(
txHash = value.id,

View file

@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.utils.toLce
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
@ -12,19 +11,16 @@ import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles
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.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import kotlinx.coroutines.flow.map
@Suppress("LongParameterList")
internal class MultiWalletTokenListSubscriber(
private val userWallet: UserWallet,
private val getTokenListUseCase: GetTokenListUseCase,
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val walletFeatureToggles: WalletFeatureToggles,
stateHolder: WalletStateController,
clickIntents: WalletClickIntents,
tokenListAnalyticsSender: TokenListAnalyticsSender,
@ -42,11 +38,7 @@ internal class MultiWalletTokenListSubscriber(
) {
override fun tokenListFlow(): LceFlow<TokenListError, TokenList> {
return if (walletFeatureToggles.isTokenListLceFlowEnabled) {
getTokenListUseCase.launchLce(userWallet.walletId)
} else {
getTokenListUseCase.launch(userWallet.walletId).map { it.toLce() }
}
return getTokenListUseCase.launch(userWallet.walletId)
}
override suspend fun onTokenListReceived(maybeTokenList: Lce<TokenListError, TokenList>) {

View file

@ -20,6 +20,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
@ -33,11 +34,12 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.components.atoms.handComposableComponentHeight
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet
@ -50,10 +52,12 @@ import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TestTags
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder
import com.tangem.feature.wallet.presentation.wallet.ui.components.PushNotificationsBottomSheet
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.*
@ -65,16 +69,13 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.VisaTxDe
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balancesAndLimitsBlock
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.depositButton
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
import com.tangem.features.managetokens.navigation.ExpandableState
import com.tangem.features.markets.component.BottomSheetState
import com.tangem.features.markets.component.MarketsListComponent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
@Composable
internal fun WalletScreen(
state: WalletScreenState,
bottomSheetHeaderHeightProvider: () -> Dp,
bottomSheetContent: @Composable () -> Unit,
) {
internal fun WalletScreen(state: WalletScreenState, marketsListComponent: MarketsListComponent?) {
BackHandler(onBack = state.onBackClick)
// It means that screen is still initializing
@ -97,8 +98,7 @@ internal fun WalletScreen(
snackbarHostState = snackbarHostState,
isAutoScroll = isAutoScroll,
onAutoScrollReset = { isAutoScroll.value = false },
bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider,
bottomSheetContent = bottomSheetContent,
marketsListComponent = marketsListComponent,
alertConfig = alertConfig,
)
@ -112,17 +112,16 @@ internal fun WalletScreen(
)
}
@Suppress("LongMethod", "LongParameterList")
@Suppress("LongMethod", "LongParameterList", "CyclomaticComplexMethod")
@Composable
private fun WalletContent(
state: WalletScreenState,
walletsListState: LazyListState,
snackbarHostState: SnackbarHostState,
isAutoScroll: State<Boolean>,
bottomSheetHeaderHeightProvider: () -> Dp,
onAutoScrollReset: () -> Unit,
bottomSheetContent: @Composable () -> Unit,
marketsListComponent: MarketsListComponent?,
alertConfig: WalletAlertState?,
onAutoScrollReset: () -> Unit,
) {
var selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) }
val selectedWallet = state.wallets.getOrElse(selectedWalletIndex) { state.wallets[state.selectedWalletIndex] }
@ -144,17 +143,20 @@ private fun WalletContent(
.padding(top = betweenItemsPadding)
.padding(horizontal = horizontalPadding)
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
LazyColumn(
modifier = Modifier
.fillMaxSize()
.testTag(TestTags.WALLET_SCREEN),
contentPadding = PaddingValues(
bottom = TangemTheme.dimens.spacing92,
bottom = TangemTheme.dimens.spacing92 + bottomBarHeight,
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
item(
key = state.wallets.map { it.walletCardState.id },
// !!! Type of the key should be saveable via Bundle on Android !!!
key = state.wallets.map { it.walletCardState.id.stringValue },
contentType = state.wallets.map { it.walletCardState.id },
) {
WalletsList(
@ -202,17 +204,7 @@ private fun WalletContent(
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)
is BalancesAndLimitsBottomSheetConfig -> BalancesAndLimitsBottomSheet(config = bottomSheetConfig)
is VisaTxDetailsBottomSheetConfig -> VisaTxDetailsBottomSheet(config = bottomSheetConfig)
}
}
ShowBottomSheet(bottomSheetConfig = selectedWallet.bottomSheetConfig)
WalletsListEffects(
lazyListState = walletsListState,
@ -224,14 +216,28 @@ private fun WalletContent(
)
}
if (state.manageTokenRedesignToggle) {
BaseScaffoldManageTokenRedesign(
if (marketsListComponent != null) {
val bottomSheetState = remember {
mutableStateOf(BottomSheetState.COLLAPSED)
}
var headerSize by remember {
mutableStateOf(0.dp)
}
BaseScaffoldWithMarkets(
state = state,
selectedWallet = selectedWallet,
snackbarHostState = snackbarHostState,
bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider,
bottomSheetContent = bottomSheetContent,
bottomSheetHeaderHeightProvider = { headerSize },
alertConfig = alertConfig,
onBottomSheetStateChange = { bottomSheetState.value = it },
bottomSheetContent = {
marketsListComponent.BottomSheetContent(
bottomSheetState = bottomSheetState,
onHeaderSizeChange = { headerSize = it },
modifier = Modifier,
)
},
) {
scaffoldContent()
}
@ -246,16 +252,78 @@ private fun WalletContent(
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun BaseScaffold(
state: WalletScreenState,
selectedWallet: WalletState,
snackbarHostState: SnackbarHostState,
content: @Composable () -> Unit,
) {
Scaffold(
topBar = { WalletTopBar(config = state.topBarConfig) },
contentWindowInsets = WindowInsetsZero,
snackbarHost = {
WalletSnackbarHost(
snackbarHostState = snackbarHostState,
event = state.event,
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16),
)
},
floatingActionButton = {
val manageTokensButtonConfig by remember(state.selectedWalletIndex) {
mutableStateOf(
(state.wallets[state.selectedWalletIndex] as? WalletState.MultiCurrency)?.manageTokensButtonConfig,
)
}
manageTokensButtonConfig?.let {
ManageTokensButton(
modifier = Modifier.navigationBarsPadding(),
onClick = it.onClick,
)
}
},
floatingActionButtonPosition = FabPosition.Center,
containerColor = TangemTheme.colors.background.secondary,
content = {
val pullRefreshState = rememberPullRefreshState(
refreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
onRefresh = {
selectedWallet.pullToRefreshConfig.onRefresh(WalletPullToRefreshConfig.ShowRefreshState(true))
},
)
Box(
modifier = Modifier
.pullRefresh(pullRefreshState)
.padding(it),
) {
content()
WalletPullToRefreshIndicator(
isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
)
BottomFade(Modifier.align(Alignment.BottomCenter))
}
},
)
}
@Suppress("LongParameterList", "LongMethod")
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
@Composable
private fun BaseScaffoldManageTokenRedesign(
private fun BaseScaffoldWithMarkets(
state: WalletScreenState,
selectedWallet: WalletState,
snackbarHostState: SnackbarHostState,
bottomSheetHeaderHeightProvider: () -> Dp,
bottomSheetContent: @Composable () -> Unit,
alertConfig: WalletAlertState?,
onBottomSheetStateChange: (BottomSheetState) -> Unit,
content: @Composable () -> Unit,
) {
// show the bottom sheet if there is at least one multicurrency wallet
@ -278,10 +346,10 @@ private fun BaseScaffoldManageTokenRedesign(
BottomSheetStateEffects(
bottomSheetState = bottomSheetState,
state = state,
showManageTokensBottomSheet = showManageTokensBottomSheet,
alertConfig = alertConfig,
keyboardShown = keyboardShown,
onBottomSheetStateChange = onBottomSheetStateChange,
)
val scaffoldState = rememberBottomSheetScaffoldState(
@ -310,6 +378,8 @@ private fun BaseScaffoldManageTokenRedesign(
sheetDragHandle = {
Hand(modifier = Modifier.background(color = TangemTheme.colors.background.primary))
},
sheetTonalElevation = 8.dp,
sheetShadowElevation = 8.dp,
sheetContent = {
BoxWithConstraints {
Box(
@ -329,7 +399,7 @@ private fun BaseScaffoldManageTokenRedesign(
coroutineScope.launch { bottomSheetState.partialExpand() }
}
},
content = { paddingValues ->
content = { _ ->
val pullRefreshState = rememberPullRefreshState(
refreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
onRefresh = {
@ -337,9 +407,7 @@ private fun BaseScaffoldManageTokenRedesign(
},
)
Column(
modifier = Modifier.padding(paddingValues),
) {
Column {
WalletTopBar(config = state.topBarConfig)
Box(
modifier = Modifier.pullRefresh(pullRefreshState),
@ -391,14 +459,14 @@ private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: (
}
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("CyclomaticComplexMethod")
@Suppress("CyclomaticComplexMethod", "MagicNumber", "LongMethod")
@Composable
private fun BottomSheetStateEffects(
bottomSheetState: SheetState,
state: WalletScreenState,
showManageTokensBottomSheet: Boolean,
alertConfig: WalletAlertState?,
keyboardShown: State<Keyboard>,
onBottomSheetStateChange: (BottomSheetState) -> Unit,
) {
// Bottom sheet during initialization internally expand partially after its content was remeasured,
// therefore initialValue = SheetValue.Hidden in rememberStandardBottomSheetState doesn't work as expected
@ -424,19 +492,29 @@ private fun BottomSheetStateEffects(
val systemUiController = rememberSystemUiController()
val navigationBarColor = TangemTheme.colors.background.primary
val navigationBarColorWithout = TangemTheme.colors.background.secondary
SystemBarsEffect {
if (showManageTokensBottomSheet) {
setNavigationBarColor(navigationBarColor)
LaunchedEffect(key1 = bottomSheetState.targetValue, navigationBarColor) {
when (bottomSheetState.targetValue) {
SheetValue.Hidden,
SheetValue.Expanded,
-> systemUiController.setNavigationBarColor(
color = Color.Transparent,
darkIcons = navigationBarColor.luminance() > 0.5f,
navigationBarContrastEnforced = true,
)
SheetValue.PartiallyExpanded,
-> systemUiController.setNavigationBarColor(navigationBarColor)
}
}
DisposableEffect(
showManageTokensBottomSheet,
) {
DisposableEffect(showManageTokensBottomSheet) {
onDispose {
if (showManageTokensBottomSheet) {
systemUiController.setNavigationBarColor(navigationBarColorWithout)
systemUiController.setNavigationBarColor(
color = Color.Transparent,
darkIcons = navigationBarColor.luminance() > 0.5f,
navigationBarContrastEnforced = false,
)
}
}
}
@ -463,11 +541,13 @@ private fun BottomSheetStateEffects(
val isSheetHidden = bottomSheetState.targetValue == SheetValue.PartiallyExpanded
LaunchedEffect(isSheetHidden) {
if (isSheetHidden) {
state.manageTokensExpandableState.value = ExpandableState.COLLAPSED
} else {
state.manageTokensExpandableState.value = ExpandableState.EXPANDED
}
onBottomSheetStateChange(
if (isSheetHidden) {
BottomSheetState.COLLAPSED
} else {
BottomSheetState.EXPANDED
},
)
}
}
@ -495,59 +575,6 @@ private fun rememberSheetStateEnhanced(
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun BaseScaffold(
state: WalletScreenState,
selectedWallet: WalletState,
snackbarHostState: SnackbarHostState,
content: @Composable () -> Unit,
) {
Scaffold(
topBar = { WalletTopBar(config = state.topBarConfig) },
snackbarHost = {
WalletSnackbarHost(
snackbarHostState = snackbarHostState,
event = state.event,
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16),
)
},
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(WalletPullToRefreshConfig.ShowRefreshState(true))
},
)
Box(
modifier = Modifier
.pullRefresh(pullRefreshState)
.padding(it),
) {
content()
WalletPullToRefreshIndicator(
isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
)
}
},
)
}
@Composable
private fun WalletSnackbarHost(
snackbarHostState: SnackbarHostState,
@ -564,11 +591,11 @@ private fun WalletSnackbarHost(
}
@Composable
private fun ManageTokensButton(onClick: () -> Unit) {
private fun ManageTokensButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
PrimaryButton(
text = stringResource(id = R.string.main_manage_tokens),
onClick = onClick,
modifier = Modifier
modifier = modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
)
@ -588,6 +615,21 @@ internal fun LazyListScope.organizeTokens(state: WalletState, itemModifier: Modi
}
}
@Composable
private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) {
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)
is BalancesAndLimitsBottomSheetConfig -> BalancesAndLimitsBottomSheet(config = bottomSheetConfig)
is VisaTxDetailsBottomSheetConfig -> VisaTxDetailsBottomSheet(config = bottomSheetConfig)
is PushNotificationsBottomSheetConfig -> PushNotificationsBottomSheet(config = bottomSheetConfig)
}
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@ -596,8 +638,7 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider::
TangemThemePreview {
WalletScreen(
state = data,
bottomSheetHeaderHeightProvider = { 0.dp },
bottomSheetContent = {},
marketsListComponent = null,
)
}
}

View file

@ -0,0 +1,119 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.showcase.ShowcaseContent
import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.requestPushPermission
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.PushNotificationsBottomSheetConfig
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun PushNotificationsBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<PushNotificationsBottomSheetConfig>(config = config) {
PushNotificationsSheetContent(content = it, onDismiss = config.onDismissRequest)
}
}
@Composable
private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetConfig, onDismiss: () -> Unit) {
val isClicked = remember { mutableStateOf(false) }
val requestPushPermission = requestPushPermission(
pushPermission = getPushPermissionOrNull(),
isFirstTimeAsking = content.isFirstTimeAsking,
isClicked = isClicked,
onAllow = {
content.onAllow()
onDismiss()
},
onDeny = {
content.onDeny()
onDismiss()
},
onOpenSettings = content.openSettings,
)
Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
ShowcaseContent(
headerIconRes = R.drawable.ic_notifications_unread_24,
headerText = resourceReference(R.string.user_push_notification_agreement_header),
showcaseItems = persistentListOf(
ShowcaseItemModel(
iconRes = R.drawable.ic_rocket_launch_24,
text = resourceReference(R.string.user_push_notification_agreement_argument_one),
),
ShowcaseItemModel(
iconRes = R.drawable.ic_storefront_24,
text = resourceReference(R.string.user_push_notification_agreement_argument_two),
),
),
modifier = Modifier.padding(top = TangemTheme.dimens.spacing40),
)
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
modifier = Modifier.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
top = TangemTheme.dimens.spacing40,
bottom = TangemTheme.dimens.spacing16,
),
) {
SecondaryButton(
text = stringResource(R.string.common_later),
onClick = {
content.onDeny()
onDismiss()
},
modifier = Modifier.weight(1f),
)
PrimaryButton(
text = stringResource(R.string.common_allow),
onClick = {
isClicked.value = true
content.onRequest()
requestPushPermission()
},
modifier = Modifier.weight(1f),
)
}
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PushNotificationsSheetContent_Preview() {
TangemThemePreview {
PushNotificationsSheetContent(
PushNotificationsBottomSheetConfig(
isFirstTimeAsking = false,
onRequest = {},
onAllow = {},
onDeny = {},
openSettings = {},
),
onDismiss = {},
)
}
}
// endregion

View file

@ -42,7 +42,6 @@ import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.ConstraintLayoutScope
import androidx.constraintlayout.compose.Dimension
import com.tangem.common.Strings
import com.tangem.core.ui.components.FontSizeRange
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.ResizableText
@ -54,6 +53,7 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
import com.tangem.utils.StringsSigns
private const val HALF_OF_ITEM_WIDTH = 0.5
@ -291,7 +291,7 @@ private fun Balance(state: WalletCardState, isBalanceHidden: Boolean, modifier:
when (walletCardState) {
is WalletCardState.Content -> {
ResizableText(
text = if (isBalanceHidden) Strings.STARS else walletCardState.balance,
text = if (isBalanceHidden) StringsSigns.STARS else walletCardState.balance,
fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize),
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32),
color = TangemTheme.colors.text.primary1,

View file

@ -1,15 +1,13 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels
import androidx.compose.runtime.MutableState
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.IsWalletsScrollPreviewEnabled
import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase
import com.tangem.domain.settings.*
import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
@ -21,6 +19,7 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWal
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.model.PushNotificationsBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent.DemonstrateWalletsScrollPreview.Direction
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
@ -28,7 +27,9 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.*
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import com.tangem.features.managetokens.navigation.ExpandableState
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
@ -56,13 +57,18 @@ internal class WalletViewModel @Inject constructor(
private val canUseBiometryUseCase: CanUseBiometryUseCase,
private val isWalletsScrollPreviewEnabled: IsWalletsScrollPreviewEnabled,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
analyticsEventsHandler: AnalyticsEventHandler,
private val dispatchers: CoroutineDispatcherProvider,
private val screenLifecycleProvider: ScreenLifecycleProvider,
private val selectedWalletAnalyticsSender: SelectedWalletAnalyticsSender,
private val walletDeepLinksHandler: WalletDeepLinksHandler,
private val walletNameMigrationUseCase: WalletNameMigrationUseCase,
private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase,
private val shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase,
private val isFirstTimeAskingPermissionUseCase: IsFirstTimeAskingPermissionUseCase,
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles,
private val settingsManager: SettingsManager,
analyticsEventsHandler: AnalyticsEventHandler,
) : ViewModel() {
val uiState: StateFlow<WalletScreenState> = stateHolder.uiState
@ -82,6 +88,7 @@ internal class WalletViewModel @Inject constructor(
subscribeOnBalanceHiding()
subscribeOnSelectedWalletFlow()
subscribeToScreenBackgroundState()
subscribeOnPushNotificationsPermission()
}
private fun maybeMigrateNames() {
@ -95,12 +102,6 @@ internal class WalletViewModel @Inject constructor(
clickIntents.initialize(router, viewModelScope)
}
fun setExpandableState(state: MutableState<ExpandableState>) {
stateHolder.update {
it.copy(manageTokensExpandableState = state)
}
}
fun subscribeToLifecycle(lifecycleOwner: LifecycleOwner) {
lifecycleOwner.lifecycle.addObserver(screenLifecycleProvider)
}
@ -147,6 +148,34 @@ internal class WalletViewModel @Inject constructor(
.launchIn(viewModelScope)
}
private fun subscribeOnPushNotificationsPermission() {
viewModelScope.launch {
val isPushToggled = pushNotificationsFeatureToggles.isPushNotificationsEnabled
val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION)
val isPushPermissionAvailable = getPushPermissionOrNull() != null
if (!isPushToggled || !shouldRequestPush || !isPushPermissionAvailable) return@launch
delay(timeMillis = 1_800)
val isFirstTimeAsking = isFirstTimeAskingPermissionUseCase(PUSH_PERMISSION).getOrElse { true }
val wasInitiallyAsk = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrElse { true }
val onDenyClick: () -> Unit = if (wasInitiallyAsk) {
clickIntents::onDelayAskPushPermission
} else {
clickIntents::onNeverAskPushPermission
}
stateHolder.showBottomSheet(
PushNotificationsBottomSheetConfig(
isFirstTimeAsking = isFirstTimeAsking,
onRequest = clickIntents::onRequestPushPermission,
onAllow = clickIntents::onNeverAskPushPermission,
onDeny = onDenyClick,
openSettings = settingsManager::openSettings,
),
)
}
}
private fun subscribeOnSelectedWalletFlow() {
getSelectedWalletUseCase().onRight {
it

View file

@ -1,21 +1,16 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.wallets.usecase.GetWalletNamesUseCase
import com.tangem.domain.wallets.usecase.RenameWalletUseCase
import com.tangem.feature.wallet.impl.R
import com.tangem.domain.card.DeleteSavedAccessCodesUseCase
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.ReduxNavController
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.*
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
@ -52,7 +47,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor(
private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val reduxStateHolder: ReduxStateHolder,
private val reduxNavController: ReduxNavController,
private val appRouter: AppRouter,
private val dispatchers: CoroutineDispatcherProvider,
) : BaseWalletClickIntents(), WalletCardClickIntents {
@ -123,7 +118,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor(
reduxStateHolder.onUserWalletSelected(selectedWallet)
} else {
stateHolder.clear()
reduxNavController.navigate(NavigationAction.PopBackTo(AppScreen.Home))
appRouter.replaceAll(AppRoute.Home)
}
}
}

View file

@ -32,6 +32,7 @@ internal class WalletClickIntents @Inject constructor(
private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor,
private val contentClickIntentsImplementor: WalletContentClickIntentsImplementor,
private val visaWalletIntentsImplementor: VisaWalletIntentsImplementor,
private val pushPermissionClickIntentsImplementor: WalletPushPermissionClickIntentsImplementor,
private val stateHolder: WalletStateController,
private val walletScreenContentLoader: WalletScreenContentLoader,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
@ -48,7 +49,8 @@ internal class WalletClickIntents @Inject constructor(
WalletWarningsClickIntents by warningsClickIntentsImplementer,
WalletCurrencyActionsClickIntents by currencyActionsClickIntentsImplementor,
WalletContentClickIntents by contentClickIntentsImplementor,
VisaWalletIntents by visaWalletIntentsImplementor {
VisaWalletIntents by visaWalletIntentsImplementor,
WalletPushPermissionClickIntents by pushPermissionClickIntentsImplementor {
override fun initialize(router: InnerWalletRouter, coroutineScope: CoroutineScope) {
super.initialize(router, coroutineScope)
@ -58,6 +60,7 @@ internal class WalletClickIntents @Inject constructor(
currencyActionsClickIntentsImplementor.initialize(router, coroutineScope)
contentClickIntentsImplementor.initialize(router, coroutineScope)
visaWalletIntentsImplementor.initialize(router, coroutineScope)
pushPermissionClickIntentsImplementor.initialize(router, coroutineScope)
}
fun onWalletChange(index: Int) {

View file

@ -44,6 +44,7 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
interface WalletCurrencyActionsClickIntents {
@ -58,6 +59,8 @@ interface WalletCurrencyActionsClickIntents {
fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference?
fun onCopyAddressClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
@ -364,6 +367,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
showErrorIfDemoModeOrElse(action = ::openExplorer)
}
override fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {
// TODO staking
Timber.e("Not implemented yet")
}
private fun openExplorer() {
val userWalletId = stateHolder.getSelectedWalletId()
@ -466,6 +474,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
private fun getUnavailabilityReasonText(unavailabilityReason: ScenarioUnavailabilityReason): TextReference {
return when (unavailabilityReason) {
is ScenarioUnavailabilityReason.StakingUnavailable -> {
resourceReference(
id = R.string.token_button_unavailability_reason_staking_unavailable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
is ScenarioUnavailabilityReason.PendingTransaction -> {
when (unavailabilityReason.withdrawalScenario) {
ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference(

View file

@ -0,0 +1,44 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
import com.tangem.domain.settings.DelayPermissionRequestUseCase
import com.tangem.domain.settings.NeverRequestPermissionUseCase
import com.tangem.domain.settings.SetFirstTimeAskingPermissionUseCase
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.launch
import javax.inject.Inject
internal interface WalletPushPermissionClickIntents {
fun onRequestPushPermission()
fun onDelayAskPushPermission()
fun onNeverAskPushPermission()
}
@ViewModelScoped
internal class WalletPushPermissionClickIntentsImplementor @Inject constructor(
private val setFirstTimeAskingPermissionUseCase: SetFirstTimeAskingPermissionUseCase,
private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase,
private val delayPermissionRequestUseCase: DelayPermissionRequestUseCase,
) : BaseWalletClickIntents(), WalletPushPermissionClickIntents {
override fun onRequestPushPermission() {
viewModelScope.launch {
setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION)
}
}
override fun onDelayAskPushPermission() {
viewModelScope.launch {
delayPermissionRequestUseCase(PUSH_PERMISSION)
}
}
override fun onNeverAskPushPermission() {
viewModelScope.launch {
neverRequestPermissionUseCase(PUSH_PERMISSION)
}
}
}

View file

@ -165,7 +165,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
override fun onScanToUnlockWalletClick() {
analyticsEventHandler.send(MainScreen.UnlockWithCardScan)
openScanCardDialog()
}
private fun openScanCardDialog() {
viewModelScope.launch(dispatchers.main) {
scanCardToUnlockWalletClickHandler(walletId = stateHolder.getSelectedWalletId())
.onLeft { error ->
@ -175,7 +178,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
event = WalletEvent.ShowAlert(WalletAlertState.WrongCardIsScanned),
)
}
ScanCardToUnlockWalletError.ManyScanFails -> router.openScanFailedDialog()
ScanCardToUnlockWalletError.ManyScanFails -> router.openScanFailedDialog(::openScanCardDialog)
}
}
}
@ -201,7 +204,12 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
viewModelScope.launch(dispatchers.main) {
neverToSuggestRateAppUseCase()
reduxStateHolder.dispatch(LegacyAction.SendEmailRateCanBeBetter)
reduxStateHolder.dispatch(
LegacyAction.SendEmailRateCanBeBetter(
scanResponse = getSelectedUserWallet()?.scanResponse
?: error("ScanResponse must be not null"),
),
)
}
}
@ -254,7 +262,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
}
}
private suspend fun getSelectedUserWallet(): UserWallet? {
private fun getSelectedUserWallet(): UserWallet? {
val userWalletId = stateHolder.getSelectedWalletId()
return getUserWalletUseCase(userWalletId).getOrElse {
Timber.e(