Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-28 13:36:01 +08:00
parent ef19a3697d
commit db4efb71a1
15 changed files with 595 additions and 314 deletions

View file

@ -5,116 +5,126 @@ import com.tangem.domain.wallets.models.UserWalletId
internal object MockTokens {
val token1 = Token(
id = Token.ID("token1"),
networkId = MockNetworks.network1.id,
name = "Token 1",
symbol = "T1",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = null,
derivationPath = null,
)
val token2 = Token(
id = Token.ID("token2"),
networkId = MockNetworks.network1.id,
name = "Token 2",
symbol = "T2",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token3 = Token(
id = Token.ID("token3"),
networkId = MockNetworks.network1.id,
name = "Token 3",
symbol = "T3",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token4 = Token(
id = Token.ID("token4"),
networkId = MockNetworks.network2.id,
name = "Token 4",
symbol = "T4",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = null,
derivationPath = null,
)
val token5 = Token(
id = Token.ID("token5"),
networkId = MockNetworks.network2.id,
name = "Token 5",
symbol = "T5",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token6 = Token(
id = Token.ID("token6"),
networkId = MockNetworks.network2.id,
name = "Token 6",
symbol = "T6",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token7 = Token(
id = Token.ID("token7"),
networkId = MockNetworks.network3.id,
name = "Token 7",
symbol = "T7",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = null,
derivationPath = null,
)
val token8 = Token(
id = Token.ID("token8"),
networkId = MockNetworks.network3.id,
name = "Token 8",
symbol = "T8",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token9 = Token(
id = Token.ID("token9"),
networkId = MockNetworks.network3.id,
name = "Token 9",
symbol = "T9",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token10 = Token(
id = Token.ID("token10"),
networkId = MockNetworks.network3.id,
name = "Token 10",
symbol = "T10",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token1
get() = Token(
id = Token.ID("token1"),
networkId = MockNetworks.network1.id,
name = "Token 1",
symbol = "T1",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = null,
derivationPath = null,
)
val token2
get() = Token(
id = Token.ID("token2"),
networkId = MockNetworks.network1.id,
name = "Token 2",
symbol = "T2",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token3
get() = Token(
id = Token.ID("token3"),
networkId = MockNetworks.network1.id,
name = "Token 3",
symbol = "T3",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token4
get() = Token(
id = Token.ID("token4"),
networkId = MockNetworks.network2.id,
name = "Token 4",
symbol = "T4",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = null,
derivationPath = null,
)
val token5
get() = Token(
id = Token.ID("token5"),
networkId = MockNetworks.network2.id,
name = "Token 5",
symbol = "T5",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token6
get() = Token(
id = Token.ID("token6"),
networkId = MockNetworks.network2.id,
name = "Token 6",
symbol = "T6",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token7
get() = Token(
id = Token.ID("token7"),
networkId = MockNetworks.network3.id,
name = "Token 7",
symbol = "T7",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = null,
derivationPath = null,
)
val token8
get() = Token(
id = Token.ID("token8"),
networkId = MockNetworks.network3.id,
name = "Token 8",
symbol = "T8",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token9
get() = Token(
id = Token.ID("token9"),
networkId = MockNetworks.network3.id,
name = "Token 9",
symbol = "T9",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token10
get() = Token(
id = Token.ID("token10"),
networkId = MockNetworks.network3.id,
name = "Token 10",
symbol = "T10",
isCustom = false,
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val tokens
get() = mapOf(

View file

@ -45,6 +45,7 @@ dependencies {
/** Domain modules */
implementation(projects.common)
implementation(projects.domain.card)
implementation(projects.domain.demo)
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.settings)

View file

@ -74,11 +74,15 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation
}
override fun openOrganizeTokensScreen() {
navController.navigate(WalletScreens.ORGANIZE_TOKENS.name)
navController.navigate(route = WalletScreens.ORGANIZE_TOKENS.name)
}
override fun openDetailsScreen() {
navigationStateHolder.navigate(NavigationAction.NavigateTo(AppScreen.Details))
navigationStateHolder.navigate(action = NavigationAction.NavigateTo(AppScreen.Details))
}
override fun openOnboardingScreen() {
navigationStateHolder.navigate(action = NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
}
private companion object {

View file

@ -33,4 +33,7 @@ internal interface InnerWalletRouter : WalletRouter {
/** Open details screen */
fun openDetailsScreen()
/** Open onboarding screen */
fun openOnboardingScreen()
}

View file

@ -29,10 +29,14 @@ internal data class WalletBottomSheetConfig(
@DrawableRes open val iconResId: Int,
open val tint: Color? = null,
val primaryButtonConfig: ButtonConfig,
val secondaryButtonConfig: ButtonConfig? = null,
val secondaryButtonConfig: ButtonConfig,
) {
data class ButtonConfig(val text: String, val onClick: () -> Unit, @DrawableRes val iconResId: Int? = null)
data class ButtonConfig(
val text: TextReference,
val onClick: () -> Unit,
@DrawableRes val iconResId: Int? = null,
)
data class UnlockWallets(
val onUnlockClick: () -> Unit,
@ -45,9 +49,9 @@ internal data class WalletBottomSheetConfig(
),
iconResId = R.drawable.ic_locked_24,
tint = TangemColorPalette.Black,
primaryButtonConfig = ButtonConfig(text = "Unlock", onClick = onUnlockClick),
primaryButtonConfig = ButtonConfig(text = TextReference.Str(value = "Unlock"), onClick = onUnlockClick),
secondaryButtonConfig = ButtonConfig(
text = "Scan card",
text = TextReference.Str(value = "Scan card"),
onClick = onScanClick,
iconResId = R.drawable.ic_tangem_24,
),
@ -61,19 +65,35 @@ internal data class WalletBottomSheetConfig(
subtitle = TextReference.Str(value = "How was your experience with our app? Let us know:"),
iconResId = R.drawable.ic_star_24,
tint = TangemColorPalette.Tangerine,
primaryButtonConfig = ButtonConfig(text = "Rate the app", onClick = onRateTheAppClick),
secondaryButtonConfig = ButtonConfig(text = "Share feedback", onClick = onShareClick),
primaryButtonConfig = ButtonConfig(
text = TextReference.Str(value = "Rate the app"),
onClick = onRateTheAppClick,
),
secondaryButtonConfig = ButtonConfig(
text = TextReference.Str(value = "Share feedback"),
onClick = onShareClick,
),
)
data class MultiWalletAlreadySignedHashes(val onLearnClick: () -> Unit) : BottomSheetContentConfig(
data class CriticalWarningAlreadySignedHashes(
val onOkClick: () -> Unit,
val onCancelClick: () -> Unit,
) : BottomSheetContentConfig(
title = TextReference.Res(
id = R.string.warning_important_security_info,
formatArgs = WrappedList(listOf("\u26A0")),
),
subtitle = TextReference.Res(id = R.string.warning_signed_tx_previously),
subtitle = TextReference.Res(id = R.string.alert_signed_hashes_message),
iconResId = R.drawable.img_attention_20,
tint = null,
primaryButtonConfig = ButtonConfig(text = "Learn more", onClick = onLearnClick),
primaryButtonConfig = ButtonConfig(
text = TextReference.Res(id = R.string.common_ok),
onClick = onOkClick,
),
secondaryButtonConfig = ButtonConfig(
text = TextReference.Res(id = R.string.common_cancel),
onClick = onCancelClick,
),
)
}
}

View file

@ -81,26 +81,26 @@ sealed class WalletNotification(open val state: NotificationState) {
)
/**
* "Wallet already signed hashes" notification
* "Already topped up and signed hashes" warning notification
*
* @property onClick lambda be invoked when notification is clicked
* @property onClick lambda be invoked when notification's close button is clicked
*/
data class AlreadyToppedUpAndSignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification(
state = NotificationState.Clickable(
data class WarningAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification(
state = NotificationState.Closable(
title = TextReference.Res(id = R.string.common_warning),
subtitle = TextReference.Res(id = R.string.alert_card_signed_transactions),
iconResId = R.drawable.img_attention_20,
onClick = onClick,
tint = null,
onCloseClick = onClick,
),
)
/**
* "Multi wallet already signed hashes" notification
* "Already signed hashes" critical warning notification
*
* @property onClick lambda be invoked when notification is clicked
*/
data class SignedTransactionsInThePast(override val onClick: () -> Unit) : Clickable, WalletNotification(
data class CriticalWarningAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification(
state = NotificationState.Clickable(
title = TextReference.Res(
id = R.string.warning_important_security_info,

View file

@ -0,0 +1,53 @@
package com.tangem.feature.wallet.presentation.wallet.state.factory
import arrow.core.Either
import com.tangem.common.Provider
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.TokenList
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder
import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletLoadedTokensListConverter.LoadedTokensListModel
import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorToWalletStateConverter
import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter
import com.tangem.utils.converter.Converter
/**
* Converter from loaded [TokenListError] or [TokenList] to [WalletStateHolder]
*
* @property currentStateProvider current ui state provider
*
[REDACTED_AUTHOR]
*/
internal class WalletLoadedTokensListConverter(
private val currentStateProvider: Provider<WalletStateHolder>,
) : Converter<LoadedTokensListModel, WalletStateHolder> {
private val tokenListStateConverter = TokenListToWalletStateConverter(
currentState = currentStateProvider(),
isWalletContentHidden = false, // TODO: [REDACTED_JIRA]
fiatCurrencyCode = "USD", // TODO: [REDACTED_JIRA]
fiatCurrencySymbol = "$", // TODO: [REDACTED_JIRA]
)
private val tokenListErrorStateConverter = TokenListErrorToWalletStateConverter(
currentState = currentStateProvider(),
)
override fun convert(value: LoadedTokensListModel): WalletStateHolder {
return value.tokenListEither.fold(
ifLeft = tokenListErrorStateConverter::convert,
ifRight = {
tokenListStateConverter.convert(
value = TokenListToWalletStateConverter.TokensListModel(
tokenList = it,
isRefreshing = value.isRefreshing,
),
)
},
)
}
data class LoadedTokensListModel(
val tokenListEither: Either<TokenListError, TokenList>,
val isRefreshing: Boolean,
)
}

View file

@ -1,85 +1,75 @@
package com.tangem.feature.wallet.presentation.wallet.state.builder
package com.tangem.feature.wallet.presentation.wallet.state.factory
import com.tangem.common.Provider
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
import com.tangem.feature.wallet.presentation.wallet.state.*
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickCallbacks
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
internal class WalletStateFactory(
private val routerProvider: Provider<InnerWalletRouter>,
private val onScanCardClick: () -> Unit,
private val onWalletChange: (Int) -> Unit,
private val onRefreshSwipe: () -> Unit,
) {
/**
* Converter from loaded list of [UserWallet] to skeleton state of screen [WalletStateHolder]
*
* @property clickCallbacks screen click callbacks
*
[REDACTED_AUTHOR]
*/
internal class WalletSkeletonStateConverter(
private val clickCallbacks: WalletClickCallbacks,
) : Converter<List<UserWallet>, WalletStateHolder> {
fun getInitialState(): WalletStateHolder = WalletStateHolder.Loading(onBackClick = ::onBackClick)
fun getContentState(wallets: List<UserWallet>): WalletStateHolder {
val cardTypeResolver = requireNotNull(wallets.firstOrNull()).scanResponse.cardTypesResolver
override fun convert(value: List<UserWallet>): WalletStateHolder {
val cardTypeResolver = requireNotNull(value.firstOrNull()).scanResponse.cardTypesResolver
return if (cardTypeResolver.isMultiwalletAllowed()) {
createMultiCurrencyState(wallets)
createMultiCurrencyState(value)
} else {
createSingleCurrencyState(wallets)
createSingleCurrencyState(value, cardTypeResolver)
}
}
private fun createMultiCurrencyState(wallets: List<UserWallet>): WalletStateHolder.MultiCurrencyContent {
return WalletStateHolder.MultiCurrencyContent(
onBackClick = ::onBackClick,
onBackClick = clickCallbacks::onBackClick,
topBarConfig = createTopBarConfig(),
walletsListConfig = createWalletsListConfig(wallets),
pullToRefreshConfig = createPullToRefreshConfig(),
contentItems = persistentListOf(),
notifications = persistentListOf(),
bottomSheet = WalletBottomSheetConfig(
// TODO: check notifications
isShow = false,
onDismissRequest = {},
content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp(
onRateTheAppClick = {},
onShareClick = {},
),
),
onOrganizeTokensClick = routerProvider()::openOrganizeTokensScreen,
bottomSheet = null,
onOrganizeTokensClick = clickCallbacks::onOrganizeTokensClick,
)
}
private fun createSingleCurrencyState(wallets: List<UserWallet>): WalletStateHolder.SingleCurrencyContent {
private fun createSingleCurrencyState(
wallets: List<UserWallet>,
cardTypeResolver: CardTypesResolver,
): WalletStateHolder.SingleCurrencyContent {
return WalletStateHolder.SingleCurrencyContent(
onBackClick = ::onBackClick,
onBackClick = clickCallbacks::onBackClick,
topBarConfig = createTopBarConfig(),
walletsListConfig = createWalletsListConfig(wallets),
pullToRefreshConfig = createPullToRefreshConfig(),
contentItems = persistentListOf(),
notifications = persistentListOf(),
bottomSheet = WalletBottomSheetConfig(
// TODO: check notifications
isShow = false,
onDismissRequest = {},
content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp(
onRateTheAppClick = {},
onShareClick = {},
),
),
bottomSheet = null,
buttons = WalletPreviewData.singleWalletScreenState.buttons, // TODO: create buttons
// TODO: create market price block
marketPriceBlockState = WalletPreviewData.singleWalletScreenState.marketPriceBlockState,
marketPriceBlockState = MarketPriceBlockState.Loading(
currencyName = cardTypeResolver.getBlockchain().currency,
),
)
}
private fun onBackClick() = routerProvider().popBackStack()
private fun createTopBarConfig(): WalletTopBarConfig {
return WalletTopBarConfig(
onScanCardClick = onScanCardClick,
onMoreClick = routerProvider()::openDetailsScreen,
onScanCardClick = clickCallbacks::onScanCardClick,
onMoreClick = clickCallbacks::onDetailsClick,
)
}
@ -88,22 +78,22 @@ internal class WalletStateFactory(
selectedWalletIndex = 0,
wallets = wallets.map { wallet ->
WalletCardState.Loading(
id = if (wallet.scanResponse.cardTypesResolver.isMultiwalletAllowed()) { // TODO
// TODO remove after adding release logic for GetTokenListUseCase
id = if (wallet.scanResponse.cardTypesResolver.isMultiwalletAllowed()) {
UserWalletId("123")
} else {
UserWalletId("321")
},
// TODO: wallet.walletId,
title = wallet.name,
additionalInfo = "", // TODO
additionalInfo = "", // TODO add additional info resolver
imageResId = WalletImageResolver.resolve(cardTypesResolver = wallet.scanResponse.cardTypesResolver),
)
}.toImmutableList(),
onWalletChange = onWalletChange,
onWalletChange = clickCallbacks::onWalletChange,
)
}
private fun createPullToRefreshConfig(): WalletPullToRefreshConfig {
return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = onRefreshSwipe)
return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = clickCallbacks::onRefreshSwipe)
}
}

View file

@ -0,0 +1,69 @@
package com.tangem.feature.wallet.presentation.wallet.state.factory
import arrow.core.Either
import com.tangem.common.Provider
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.state.WalletBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder
import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletLoadedTokensListConverter.LoadedTokensListModel
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickCallbacks
import kotlinx.collections.immutable.ImmutableList
/**
* Main factory for creating [WalletStateHolder]
*
* @property currentStateProvider current ui state provider
* @property clickCallbacks screen click callbacks
*/
internal class WalletStateFactory(
private val currentStateProvider: Provider<WalletStateHolder>,
private val clickCallbacks: WalletClickCallbacks,
) {
private val skeletonConverter = WalletSkeletonStateConverter(clickCallbacks = clickCallbacks)
private val loadedTokensListConverter = WalletLoadedTokensListConverter(currentStateProvider = currentStateProvider)
fun getInitialState(): WalletStateHolder = WalletStateHolder.Loading(onBackClick = clickCallbacks::onBackClick)
fun getSkeletonState(wallets: List<UserWallet>): WalletStateHolder = skeletonConverter.convert(wallets)
fun getStateByTokensList(
tokenListEither: Either<TokenListError, TokenList>,
isRefreshing: Boolean,
): WalletStateHolder {
return loadedTokensListConverter.convert(
value = LoadedTokensListModel(tokenListEither = tokenListEither, isRefreshing = isRefreshing),
)
}
fun getStateByNotifications(notifications: ImmutableList<WalletNotification>): WalletStateHolder {
return currentStateProvider().copySealed(notifications = notifications)
}
fun getStateAfterWalletChanging(index: Int): WalletStateHolder {
return currentStateProvider().let { stateHolder ->
stateHolder.copySealed(walletsListConfig = stateHolder.walletsListConfig.copy(selectedWalletIndex = index))
}
}
fun getStateAfterContentRefreshing(): WalletStateHolder {
return currentStateProvider().let { state ->
state.copySealed(pullToRefreshConfig = state.pullToRefreshConfig.copy(isRefreshing = true))
}
}
fun getStateWithOpenBottomSheet(content: WalletBottomSheetConfig.BottomSheetContentConfig): WalletStateHolder {
return currentStateProvider().let { state ->
state.copySealed(
bottomSheet = WalletBottomSheetConfig(
isShow = true,
onDismissRequest = { state.copySealed(bottomSheet = state.bottomSheet?.copy(isShow = false)) },
content = content,
),
)
}
}
}

View file

@ -81,11 +81,10 @@ private fun BottomSheetContent(config: WalletBottomSheetConfig.BottomSheetConten
Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing10)) {
val buttonModifier = Modifier.fillMaxWidth()
PrimaryButton(config = config.primaryButtonConfig, modifier = buttonModifier)
if (config.secondaryButtonConfig != null) {
SecondaryButton(config = config.secondaryButtonConfig, modifier = buttonModifier)
}
SecondaryButton(config = config.secondaryButtonConfig, modifier = buttonModifier)
}
}
}
@ -97,13 +96,13 @@ private fun PrimaryButton(
) {
if (config.iconResId == null) {
PrimaryButton(
text = config.text,
text = config.text.resolveReference(),
onClick = config.onClick,
modifier = modifier,
)
} else {
PrimaryButtonIconStart(
text = config.text,
text = config.text.resolveReference(),
iconResId = config.iconResId,
onClick = config.onClick,
modifier = modifier,
@ -118,13 +117,13 @@ private fun SecondaryButton(
) {
if (config.iconResId == null) {
SecondaryButton(
text = config.text,
text = config.text.resolveReference(),
onClick = config.onClick,
modifier = modifier,
)
} else {
SecondaryButtonIconStart(
text = config.text,
text = config.text.resolveReference(),
iconResId = config.iconResId,
onClick = config.onClick,
modifier = modifier,

View file

@ -1,10 +1,13 @@
package com.tangem.feature.wallet.presentation.wallet.utils
import com.tangem.core.ui.components.transactions.TransactionState
import com.tangem.domain.tokens.model.TokenList
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder.MultiCurrencyContent
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder.SingleCurrencyContent
import com.tangem.feature.wallet.presentation.wallet.state.WalletsListConfig
import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter.TokensListModel
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toPersistentList
@ -13,47 +16,62 @@ internal class TokenListToWalletStateConverter(
private val isWalletContentHidden: Boolean,
private val fiatCurrencyCode: String,
private val fiatCurrencySymbol: String,
) : Converter<TokenList, WalletStateHolder> {
) : Converter<TokensListModel, WalletStateHolder> {
override fun convert(value: TokenList): WalletStateHolder {
return when (currentState) {
is MultiCurrencyContent -> currentState.updateWithTokenList(value)
is SingleCurrencyContent -> currentState.updateWithTokenList(value)
is WalletStateHolder.Loading,
is WalletStateHolder.UnlockWalletContent,
-> currentState
private val tokenListToContentConverter = TokenListToContentItemsConverter(
isWalletContentHidden = isWalletContentHidden,
fiatCurrencyCode = fiatCurrencyCode,
fiatCurrencySymbol = fiatCurrencySymbol,
)
override fun convert(value: TokensListModel): WalletStateHolder {
val state = if (currentState is MultiCurrencyContent) {
currentState.updateWithTokenList(tokenList = value.tokenList)
} else {
currentState
}
return state.copySealed(
walletsListConfig = state.updateSelectedWallet(value.tokenList.totalFiatBalance),
pullToRefreshConfig = if (value.isRefreshing) {
state.pullToRefreshConfig.copy(isRefreshing = state.getRefreshingStatus())
} else {
state.pullToRefreshConfig
},
)
}
private fun MultiCurrencyContent.updateWithTokenList(tokenList: TokenList): MultiCurrencyContent {
val converter = TokenListToContentItemsConverter(isWalletContentHidden, fiatCurrencyCode, fiatCurrencySymbol)
return this.copy(
walletsListConfig = updateSelectedWallet(tokenList.totalFiatBalance),
contentItems = converter.convert(tokenList),
)
}
private fun SingleCurrencyContent.updateWithTokenList(tokenList: TokenList): SingleCurrencyContent {
return this.copy(
walletsListConfig = updateSelectedWallet(tokenList.totalFiatBalance),
)
return this.copy(contentItems = tokenListToContentConverter.convert(value = tokenList))
}
private fun WalletStateHolder.updateSelectedWallet(fiatBalance: TokenList.FiatBalance): WalletsListConfig {
val selectedWalletIndex = walletsListConfig.selectedWalletIndex
val selectedWalletCard = walletsListConfig.wallets[selectedWalletIndex]
val converter = FiatBalanceToWalletCardConverter(
selectedWalletCard,
isWalletContentHidden,
fiatCurrencyCode,
fiatCurrencySymbol,
currentState = selectedWalletCard,
isWalletContentHidden = isWalletContentHidden,
fiatCurrencyCode = fiatCurrencyCode,
fiatCurrencySymbol = fiatCurrencySymbol,
)
return walletsListConfig.copy(
wallets = walletsListConfig.wallets
.toPersistentList()
.set(selectedWalletIndex, converter.convert(fiatBalance)),
.set(index = selectedWalletIndex, element = converter.convert(fiatBalance)),
)
}
private fun WalletStateHolder.getRefreshingStatus(): Boolean {
return contentItems.any { state ->
val isMultiCurrencyItem = state as? WalletContentItemState.MultiCurrencyItem.Token
val isSingleCurrencyItem = state as? WalletContentItemState.SingleCurrencyItem.Transaction
isMultiCurrencyItem?.state is TokenItemState.Loading ||
isSingleCurrencyItem?.state is TransactionState.Loading ||
state is WalletContentItemState.Loading
}
}
data class TokensListModel(val tokenList: TokenList, val isRefreshing: Boolean)
}

View file

@ -0,0 +1,21 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels
import kotlinx.coroutines.Job
/**
* Job holder. It is automatically finished old job if new one is started
*
[REDACTED_AUTHOR]
*/
internal class JobHolder {
private var job: Job? = null
/** Update current job */
fun update(job: Job) {
this.job?.cancel()
this.job = job
}
}
internal fun Job.saveIn(jobHolder: JobHolder) = jobHolder.update(job = this)

View file

@ -0,0 +1,28 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels
internal interface WalletClickCallbacks {
fun onBackClick()
fun onScanCardClick()
fun onDetailsClick()
fun onBackupCardClick()
fun onCriticalWarningAlreadySignedHashesClick()
fun onCloseWarningAlreadySignedHashesClick()
fun onLikeTangemAppClick()
fun onRateTheAppClick()
fun onShareClick()
fun onWalletChange(index: Int)
fun onRefreshSwipe()
fun onOrganizeTokensClick()
}

View file

@ -1,27 +1,38 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels
import com.tangem.common.Provider
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.tokens.model.NetworkGroup
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.model.TokenStatus
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState
import com.tangem.feature.wallet.presentation.wallet.state.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
/**
* Wallet notifications list factory
*
* @property wasCardScannedCallback callback that check if card was scanned
* @property isUserAlreadyRateAppCallback callback that check if card is user already rate app
* @property isDemoCardCallback callback that check if card is demo
* @property clickCallbacks screen click callbacks
*
[REDACTED_AUTHOR]
*/
@Suppress("LongParameterList")
internal class WalletNotificationsListFactory(
private val currentStateProvider: Provider<WalletStateHolder>,
private val wasCardScannedCallback: suspend (String) -> Boolean,
private val isUserAlreadyRateAppCallback: suspend () -> Boolean,
private val onWalletAlreadySignedHashesClick: () -> Unit,
private val onMultiWalletAlreadySignedHashesClick: () -> Unit,
private val onBackupCardClick: () -> Unit,
private val hasUnreachableNetworksCallback: () -> Boolean,
private val onLikeTangemAppClick: () -> Unit,
private val isDemoCardCallback: (String) -> Boolean,
private val clickCallbacks: WalletClickCallbacks,
) {
fun create(cardTypesResolver: CardTypesResolver): Flow<ImmutableList<WalletNotification>> {
fun create(cardTypesResolver: CardTypesResolver, tokenList: TokenList): Flow<ImmutableList<WalletNotification>> {
// TODO: [REDACTED_JIRA] order
return flow {
emit(
@ -33,31 +44,31 @@ internal class WalletNotificationsListFactory(
addRemainingSignaturesLeftNotifications(cardTypesResolver)
val isDemo = isDemoCardCallback(cardTypesResolver.getCardId())
if (!cardTypesResolver.isReleaseFirmwareType()) {
add(element = WalletNotification.DevCard)
} else {
addReleaseSpecialNotifications(cardTypesResolver)
addReleaseSpecialNotifications(cardTypesResolver = cardTypesResolver, isDemo = isDemo)
}
// TODO: [REDACTED_JIRA] - DemoModeDatasource
if (isDemo) {
add(element = WalletNotification.DemoCard)
}
if (hasUnreachableNetworksCallback()) {
if (hasUnreachableNetworks()) {
add(element = WalletNotification.UnreachableNetworks)
}
if (!cardTypesResolver.isBackupForbidden() && !cardTypesResolver.hasBackup()) {
add(element = WalletNotification.BackupCard(onClick = onBackupCardClick))
add(element = WalletNotification.BackupCard(onClick = clickCallbacks::onBackupCardClick))
}
/* TODO: [REDACTED_JIRA]
* List<WalletStoreModel>
* .filter { store ->
* store.walletsData.any { it.status is WalletDataModel.MissedDerivation }
* }
*/
if (tokenList.hasMissedDerivations()) {
add(element = WalletNotification.ScanCard(onClick = clickCallbacks::onScanCardClick))
}
if (isUserAlreadyRateAppCallback()) {
add(element = WalletNotification.LikeTangemApp(onClick = onLikeTangemAppClick))
add(element = WalletNotification.LikeTangemApp(onClick = clickCallbacks::onLikeTangemAppClick))
}
}.toImmutableList(),
)
@ -75,19 +86,21 @@ internal class WalletNotificationsListFactory(
private suspend fun MutableList<WalletNotification>.addReleaseSpecialNotifications(
cardTypesResolver: CardTypesResolver,
isDemo: Boolean,
) {
// TODO: [REDACTED_JIRA] !isDemo
if (!wasCardScannedCallback(cardTypesResolver.getCardId()) && cardTypesResolver.isMultiwalletAllowed()) {
if (!wasCardScannedCallback(cardTypesResolver.getCardId()) && cardTypesResolver.isMultiwalletAllowed() &&
!isDemo
) {
if (cardTypesResolver.isBackupForbidden() && cardTypesResolver.hasWalletSignedHashes()) {
add(
element = WalletNotification.SignedTransactionsInThePast(
onClick = onMultiWalletAlreadySignedHashesClick,
element = WalletNotification.CriticalWarningAlreadySignedHashes(
onClick = clickCallbacks::onCriticalWarningAlreadySignedHashesClick,
),
)
} else if (cardTypesResolver.hasWalletSignedHashes()) {
add(
element = WalletNotification.AlreadyToppedUpAndSignedHashes(
onClick = onWalletAlreadySignedHashesClick,
element = WalletNotification.WarningAlreadySignedHashes(
onClick = clickCallbacks::onCloseWarningAlreadySignedHashesClick,
),
)
}
@ -98,6 +111,26 @@ internal class WalletNotificationsListFactory(
}
}
private fun hasUnreachableNetworks(): Boolean {
val isUnreachableState = { item: WalletContentItemState.MultiCurrencyItem ->
(item as? WalletContentItemState.MultiCurrencyItem.Token)?.state is TokenItemState.Unreachable
}
return currentStateProvider().let { state ->
state is WalletStateHolder.MultiCurrencyContent && state.contentItems.any(isUnreachableState)
}
}
private fun TokenList.hasMissedDerivations(): Boolean {
val statuses = when (this) {
is TokenList.GroupedByNetwork -> groups.flatMap(NetworkGroup::tokens).map(TokenStatus::value)
is TokenList.Ungrouped -> tokens.map(TokenStatus::value)
TokenList.NotInitialized -> emptyList()
}
return statuses.any { it is TokenStatus.MissedDerivation }
}
private companion object {
const val MAX_REMAINING_SIGNATURES_COUNT = 10
}

View file

@ -4,30 +4,25 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.*
import arrow.core.Either
import com.tangem.common.Provider
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.core.ui.components.transactions.TransactionState
import com.tangem.domain.card.GetAccessCodeSavingStatusUseCase
import com.tangem.domain.card.GetBiometricsStatusUseCase
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.SetAccessCodeRequestPolicyUseCase
import com.tangem.domain.card.*
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import com.tangem.feature.wallet.presentation.wallet.state.*
import com.tangem.feature.wallet.presentation.wallet.state.builder.WalletStateFactory
import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorToWalletStateConverter
import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter
import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletStateFactory
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -41,74 +36,106 @@ import kotlin.properties.Delegates
@Suppress("LongParameterList")
@HiltViewModel
internal class WalletViewModel @Inject constructor(
private val getWalletsUseCase: GetWalletsUseCase,
private val saveWalletUseCase: SaveWalletUseCase,
private val getBiometricsStatusUseCase: GetBiometricsStatusUseCase,
private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase,
private val getAccessCodeSavingStatusUseCase: GetAccessCodeSavingStatusUseCase,
private val getTokenListUseCase: GetTokenListUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
private val getCardWasScannedUseCase: GetCardWasScannedUseCase,
private val isUserAlreadyRateAppUseCase: IsUserAlreadyRateAppUseCase,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val scanCardProcessor: ScanCardProcessor,
private val dispatchers: CoroutineDispatcherProvider,
) : ViewModel(), DefaultLifecycleObserver {
) : ViewModel(), DefaultLifecycleObserver, WalletClickCallbacks {
/** Feature router */
var router: InnerWalletRouter by Delegates.notNull()
private val notificationsListFactory = WalletNotificationsListFactory(
currentStateProvider = Provider { uiState },
wasCardScannedCallback = getCardWasScannedUseCase::invoke,
isUserAlreadyRateAppCallback = isUserAlreadyRateAppUseCase::invoke,
isDemoCardCallback = isDemoCardUseCase::invoke,
clickCallbacks = this,
)
private val stateFactory = WalletStateFactory(
routerProvider = Provider { router },
onScanCardClick = ::onScanCardClick,
onWalletChange = ::changeWallet,
onRefreshSwipe = ::refreshContent,
currentStateProvider = Provider { uiState },
clickCallbacks = this,
)
/** Screen state */
var uiState by mutableStateOf(stateFactory.getInitialState())
var uiState: WalletStateHolder by mutableStateOf(stateFactory.getInitialState())
private set
private var cardTypeResolverMap: Map<UserWalletId, CardTypesResolver> by Delegates.notNull()
private val getTokensJobHolder = JobHolder()
private val getNotificationJobHolder = JobHolder()
override fun onCreate(owner: LifecycleOwner) {
getWalletsUseCase()
.distinctUntilChanged()
.flowWithLifecycle(owner.lifecycle)
.distinctUntilChanged()
.onEach { wallets ->
if (wallets.isEmpty()) return@onEach
uiState = stateFactory.getContentState(wallets = wallets)
updateContentItems()
cardTypeResolverMap = wallets.associate { wallet ->
// TODO remove after adding release logic for GetTokenListUseCase
if (wallet.scanResponse.cardTypesResolver.isMultiwalletAllowed()) {
UserWalletId("123")
} else {
UserWalletId("321")
} to wallet.scanResponse.cardTypesResolver
}
uiState = stateFactory.getSkeletonState(wallets = wallets)
updateContentItems(index = 0)
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun updateContentItems() {
getTokenListUseCase(
userWalletId = uiState.walletsListConfig.wallets.get(
index = uiState.walletsListConfig.selectedWalletIndex,
).id,
private fun updateContentItems(index: Int, isRefreshing: Boolean = false) {
// TODO: use GetTransactionsUseCase
getTokenListUseCase(userWalletId = uiState.walletsListConfig.wallets[index].id)
.distinctUntilChanged()
.onEach { tokenListEither ->
uiState = stateFactory.getStateByTokensList(
tokenListEither = tokenListEither,
isRefreshing = isRefreshing,
)
tokenListEither.onRight { updateNotifications(index = index, tokenList = it) }
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
.saveIn(jobHolder = getTokensJobHolder)
}
private fun updateNotifications(index: Int, tokenList: TokenList) {
notificationsListFactory.create(
cardTypesResolver = requireNotNull(
value = cardTypeResolverMap[uiState.walletsListConfig.wallets[index].id],
lazyMessage = {
"CardTypeResolverMap doesn't contain this id = ${uiState.walletsListConfig.wallets[index].id}"
},
),
tokenList = tokenList,
)
.distinctUntilChanged()
.mapLatest(::updateStateWithTokenListOrError)
.onEach {
uiState = it.copySealed(
pullToRefreshConfig = uiState.pullToRefreshConfig.copy(isRefreshing = getRefreshingStatus()),
)
}
.onEach { uiState = stateFactory.getStateByNotifications(notifications = it) }
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
.saveIn(jobHolder = getNotificationJobHolder)
}
private fun getRefreshingStatus(): Boolean {
return uiState.contentItems.any { state ->
val isMultiCurrencyItem = state as? WalletContentItemState.MultiCurrencyItem.Token
val isSingleCurrencyItem = state as? WalletContentItemState.SingleCurrencyItem.Transaction
override fun onBackClick() = router.popBackStack()
isMultiCurrencyItem?.state is TokenItemState.Loading ||
isSingleCurrencyItem?.state is TransactionState.Loading ||
state is WalletContentItemState.Loading
}
}
private fun onScanCardClick() {
override fun onScanCardClick() {
val prevRequestPolicyStatus = getBiometricsStatusUseCase()
// Update access the code policy according access code saving status
@ -138,47 +165,52 @@ internal class WalletViewModel @Inject constructor(
}
}
private fun changeWallet(index: Int) {
override fun onDetailsClick() = router.openDetailsScreen()
override fun onBackupCardClick() = router.openOnboardingScreen()
override fun onCriticalWarningAlreadySignedHashesClick() {
uiState = stateFactory.getStateWithOpenBottomSheet(
content = WalletBottomSheetConfig.BottomSheetContentConfig.CriticalWarningAlreadySignedHashes(
onOkClick = {},
onCancelClick = {},
),
)
}
override fun onCloseWarningAlreadySignedHashesClick() {
// TODO: [REDACTED_JIRA]
}
override fun onLikeTangemAppClick() {
uiState = stateFactory.getStateWithOpenBottomSheet(
content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp(
onRateTheAppClick = ::onRateTheAppClick,
onShareClick = ::onShareClick,
),
)
}
override fun onRateTheAppClick() {
// TODO: [REDACTED_JIRA]
}
override fun onShareClick() {
// TODO: [REDACTED_JIRA]
}
override fun onWalletChange(index: Int) {
if (uiState.walletsListConfig.selectedWalletIndex == index) return
uiState = when (val state = uiState) {
is WalletStateHolder.MultiCurrencyContent -> state.copy(
walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index),
)
is WalletStateHolder.SingleCurrencyContent -> state.copy(
walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index),
)
is WalletStateHolder.UnlockWalletContent -> state.copy(
walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index),
)
is WalletStateHolder.Loading -> state
}
uiState = stateFactory.getStateAfterWalletChanging(index = index)
updateContentItems()
updateContentItems(index = index)
}
private fun refreshContent() {
uiState = uiState.copySealed(pullToRefreshConfig = uiState.pullToRefreshConfig.copy(isRefreshing = true))
updateContentItems()
override fun onRefreshSwipe() {
uiState = stateFactory.getStateAfterContentRefreshing()
updateContentItems(index = uiState.walletsListConfig.selectedWalletIndex, isRefreshing = true)
}
private fun updateStateWithTokenListOrError(tokenList: Either<TokenListError, TokenList>): WalletStateHolder {
val updateStateWithError = { error: TokenListError ->
val converter = TokenListErrorToWalletStateConverter(uiState)
converter.convert(error)
}
val updateState = { list: TokenList ->
val converter = TokenListToWalletStateConverter(
uiState,
isWalletContentHidden = false, // TODO: [REDACTED_JIRA]
fiatCurrencyCode = "USD", // TODO: [REDACTED_JIRA]
fiatCurrencySymbol = "$", // TODO: [REDACTED_JIRA]
)
converter.convert(list)
}
return tokenList.fold(ifLeft = updateStateWithError, ifRight = updateState)
}
override fun onOrganizeTokensClick() = router.openOrganizeTokensScreen()
}