Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-24 13:45:56 +03:00
commit ea661f55ab
250 changed files with 6169 additions and 949 deletions

View file

@ -12,6 +12,7 @@ dependencies {
implementation(project(":common"))
implementation(project(":domain:legacy"))
implementation(project(":core:analytics"))
implementation(projects.core.analytics.models)
implementation(project(":core:featuretoggles"))
implementation(project(":core:datasource"))
implementation(project(":core:utils"))

View file

@ -1,6 +1,6 @@
package com.tangem.feature.learn2earn.analytics
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
internal sealed class Learn2earnEvents(
category: String,

View file

@ -1,6 +1,6 @@
package com.tangem.feature.learn2earn.domain.api
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
/**
* Handler that helps determine a result of the Learn2earnWebViewActivity webView actions.

View file

@ -14,6 +14,7 @@ dependencies {
implementation(project(":core:featuretoggles"))
implementation(project(":core:datasource"))
implementation(project(":core:analytics"))
implementation(projects.core.analytics.models)
implementation(project(":core:utils"))
implementation(project(":core:ui"))
implementation(project(":core:res"))

View file

@ -54,7 +54,6 @@ private fun Content(screen: SeedPhraseScreen, uiState: OnboardingSeedPhraseState
state = uiState.introState,
)
}
SeedPhraseScreen.AboutSeedPhrase -> {
AboutSeedPhraseScreen(
state = uiState.aboutState,

View file

@ -5,6 +5,9 @@ import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.bip39.MnemonicErrorResult
import com.tangem.feature.onboarding.data.MnemonicRepository
import com.tangem.utils.extensions.isNotWhitespace
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
/**
[REDACTED_AUTHOR]
@ -58,14 +61,18 @@ internal class DefaultSeedPhraseInteractor constructor(
}
}
override suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): List<String> {
if (text.isEmpty() || cursorPosition == 0 || hasSelection) return emptyList()
override suspend fun getSuggestions(
text: String,
hasSelection: Boolean,
cursorPosition: Int,
): ImmutableList<String> {
if (text.isEmpty() || cursorPosition == 0 || hasSelection) return persistentListOf()
val word = partWordFinder.getLeadPartOfWord(text, cursorPosition)
?: return emptyList()
?: return persistentListOf()
val suggestions = repository.getWordsDictionary()
.filter { it.startsWith(word, ignoreCase = false) && it != word }
.toList()
.toPersistentList()
return suggestions
}
@ -96,7 +103,7 @@ internal class DefaultSeedPhraseInteractor constructor(
}
private fun MnemonicErrorResult.mapToError(): SeedPhraseError = when (this) {
MnemonicErrorResult.InvalidWordCount -> SeedPhraseError.InvalidEntropyLength
MnemonicErrorResult.InvalidWordCount -> SeedPhraseError.InvalidWordCount
MnemonicErrorResult.InvalidEntropyLength -> SeedPhraseError.InvalidEntropyLength
MnemonicErrorResult.InvalidWordsFile -> SeedPhraseError.InvalidWordsFile
MnemonicErrorResult.InvalidChecksum -> SeedPhraseError.InvalidChecksum

View file

@ -1,6 +1,7 @@
package com.tangem.feature.onboarding.domain
import com.tangem.crypto.bip39.Mnemonic
import kotlinx.collections.immutable.ImmutableList
/**
[REDACTED_AUTHOR]
@ -10,7 +11,7 @@ interface SeedPhraseInteractor {
suspend fun getMnemonicComponents(): Result<List<String>>
suspend fun isWordMatch(word: String): Boolean
suspend fun validateMnemonicString(text: String): Result<List<String>>
suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): List<String>
suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): ImmutableList<String>
suspend fun insertSuggestionWord(text: String, suggestion: String, cursorPosition: Int): InsertSuggestionResult
companion object {

View file

@ -1,6 +1,6 @@
package com.tangem.feature.onboarding.presentation.wallet2.analytics
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
sealed class SeedPhraseEvents(
event: String,

View file

@ -52,7 +52,7 @@ data class ImportSeedPhraseState(
val onSuggestedPhraseClick: (Int) -> Unit,
val buttonCreateWallet: ButtonState,
val invalidWords: Set<String> = emptySet(),
val suggestionsList: List<String> = emptyList(),
val suggestionsList: ImmutableList<String> = persistentListOf(),
val error: SeedPhraseError? = null,
)

View file

@ -1,6 +1,7 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui
import androidx.compose.animation.*
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material.OutlinedTextField
@ -11,10 +12,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
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.Dp
import androidx.compose.ui.unit.IntOffset
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.Notifier
import com.tangem.core.ui.components.PrimaryButtonIconStart
import com.tangem.core.ui.components.TangemTextFieldsDefault
import com.tangem.core.ui.components.buttons.common.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.presentation.wallet2.model.ImportSeedPhraseState
@ -23,6 +29,8 @@ import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Onboardi
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.InvalidWordsColorTransformation
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseErrorConverter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
[REDACTED_AUTHOR]
@ -48,7 +56,9 @@ fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Mo
state = state,
)
SuggestionsBlock(
state = state,
modifier = Modifier.padding(top = TangemTheme.dimens.size4),
suggestionsList = state.suggestionsList,
onClick = { index -> state.onSuggestedPhraseClick(index) },
)
}
}
@ -114,25 +124,86 @@ private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modif
@Suppress("ReusedModifierInstance")
@Composable
private fun SuggestionsBlock(state: ImportSeedPhraseState, modifier: Modifier = Modifier) {
private fun SuggestionsBlock(
suggestionsList: ImmutableList<String>,
onClick: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
enter = fadeIn() + slideIn(initialOffset = { IntOffset(x = 200, y = 0) }),
exit = slideOut(targetOffset = { IntOffset(x = -200, y = 0) }) + fadeOut(),
visible = state.suggestionsList.isNotEmpty(),
visible = suggestionsList.isNotEmpty(),
) {
LazyRow(
modifier = modifier.fillMaxSize(),
modifier = modifier,
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.size16),
) {
items(state.suggestionsList.size) { index ->
PrimaryButton(
modifier = Modifier
.height(TangemTheme.dimens.size46)
.padding(all = TangemTheme.dimens.size4),
text = state.suggestionsList[index],
onClick = { state.onSuggestedPhraseClick(index) },
items(suggestionsList.size) { index ->
SuggestionButton(
modifier = Modifier.rowPadding(
index = index,
rowSize = suggestionsList.size,
outSide = TangemTheme.dimens.size0,
inSide = TangemTheme.dimens.size4,
),
text = suggestionsList[index],
onClick = { onClick(index) },
)
}
}
}
}
}
@Composable
private fun SuggestionButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
Box(modifier = modifier.clickable(onClick = onClick)) {
Notifier(
text = text,
backgroundColor = TangemTheme.colors.background.action,
textColor = TangemTheme.colors.text.primary2,
)
}
}
private fun Modifier.rowPadding(index: Int, rowSize: Int, outSide: Dp, inSide: Dp): Modifier = when (index) {
0 -> this.padding(start = outSide, end = inSide)
rowSize - 1 -> this.padding(start = inSide, end = outSide)
else -> this.padding(horizontal = inSide)
}
@Preview
@Composable
private fun SuggestionsBlockPreview_Light(
@PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList<String>,
) {
TangemTheme(isDark = false) {
SuggestionsBlock(
suggestionsList = suggestions,
onClick = {},
)
}
}
@Preview
@Composable
private fun SuggestionsBlockPreview_Dark(
@PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList<String>,
) {
TangemTheme(isDark = true) {
SuggestionsBlock(
suggestionsList = suggestions,
onClick = {},
)
}
}
private class SuggestionsPreviewParamsProvider : CollectionPreviewParameterProvider<ImmutableList<String>>(
collection = listOf(
persistentListOf(
"one",
"each",
"explorer",
"unknown",
),
),
)

View file

@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.presentation.wallet2.model.DescriptionResource
@ -62,9 +63,13 @@ fun DescriptionTitleText(text: String) {
@Composable
fun DescriptionSubTitleText(text: String) {
// the text style made similar to TextViewOnboarding.Body
Text(
text = text,
style = TangemTheme.typography.subtitle1,
style = TangemTheme.typography.body1.copy(
lineHeight = 20.sp,
letterSpacing = 0.03.sp,
),
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),

View file

@ -5,6 +5,7 @@ import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.feature.onboarding.domain.InsertSuggestionResult
import com.tangem.feature.onboarding.domain.SeedPhraseError
import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState
import kotlinx.collections.immutable.ImmutableList
/**
[REDACTED_AUTHOR]
@ -39,12 +40,14 @@ class ImportSeedPhraseStateBuilder {
),
)
fun updateSuggestions(uiState: OnboardingSeedPhraseState, suggestions: List<String>): OnboardingSeedPhraseState =
uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
suggestionsList = suggestions,
),
)
fun updateSuggestions(
uiState: OnboardingSeedPhraseState,
suggestions: ImmutableList<String>,
): OnboardingSeedPhraseState = uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
suggestionsList = suggestions,
),
)
fun insertSuggestionWord(
uiState: OnboardingSeedPhraseState,

View file

@ -14,7 +14,7 @@ class SeedPhraseErrorConverter : ModuleMessageConverter<Pair<Context, SeedPhrase
val (context, message) = source
val convertedMessage = when (message) {
SeedPhraseError.InvalidEntropyLength -> {
SeedPhraseError.InvalidChecksum -> {
context.getString(R.string.onboarding_seed_mnemonic_invalid_checksum)
}
is SeedPhraseError.InvalidWords -> {

View file

@ -22,10 +22,10 @@ import com.tangem.utils.coroutines.Debouncer
import com.tangem.utils.extensions.isEven
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
/**
@ -67,7 +67,6 @@ class SeedPhraseViewModel @Inject constructor(
)
private val textFieldsDebouncers = mutableMapOf<String, Debouncer>()
private var suggestionWordInserted: AtomicBoolean = AtomicBoolean(false)
private var generatedMnemonicComponents: List<String>? = null
private var importedMnemonicComponents: List<String>? = null
@ -171,13 +170,17 @@ class SeedPhraseViewModel @Inject constructor(
if (fieldState.isError != hasError) {
updateUi { uiBuilder.checkSeedPhrase.updateTextFieldError(uiState, field, hasError) }
}
val allFieldsWithoutError = SeedPhraseField.values()
val isCreateWalletButtonEnabled = SeedPhraseField.values()
.map { field -> field.getState(uiState) }
.all { fieldState -> !fieldState.isError }
.all { fieldState -> fieldState.textFieldValue.text.isNotEmpty() && !fieldState.isError }
if (uiState.checkSeedPhraseState.buttonCreateWallet.enabled != allFieldsWithoutError) {
updateUi { uiBuilder.checkSeedPhrase.updateCreateWalletButton(uiState, allFieldsWithoutError) }
if (uiState.checkSeedPhraseState.buttonCreateWallet.enabled != isCreateWalletButtonEnabled) {
updateUi {
uiBuilder.checkSeedPhrase.updateCreateWalletButton(
uiState = uiState,
enabled = isCreateWalletButtonEnabled,
)
}
}
}
}
@ -197,19 +200,14 @@ class SeedPhraseViewModel @Inject constructor(
val debouncer = createOrGetDebouncer(MNEMONIC_DEBOUNCER)
when {
suggestionWordInserted.getAndSet(false) -> {
debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) {
validateMnemonic(inputMnemonic)
}
}
isSameText && !isCursorMoved -> {
// do nothing
}
isSameText && isCursorMoved -> {
debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) {
updateSuggestions(fieldState)
}
}
isSameText && !isCursorMoved -> {
// do nothing
}
else -> {
debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) {
updateSuggestions(fieldState)
@ -243,7 +241,6 @@ class SeedPhraseViewModel @Inject constructor(
is SeedPhraseError.InvalidWords -> {
uiBuilder.importSeedPhrase.updateInvalidWords(uiState, error.words)
}
else -> uiState
}
updateUi { uiBuilder.importSeedPhrase.updateError(mediateState, error) }
@ -366,7 +363,6 @@ class SeedPhraseViewModel @Inject constructor(
private fun buttonSuggestedPhraseClick(suggestionIndex: Int) {
viewModelScope.launchSingle {
suggestionWordInserted.set(true)
val textFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue
val word = uiState.importSeedPhraseState.suggestionsList[suggestionIndex]
val cursorPosition = textFieldValue.selection.end
@ -378,7 +374,7 @@ class SeedPhraseViewModel @Inject constructor(
)
updateUi {
val mediateState = uiBuilder.importSeedPhrase.insertSuggestionWord(uiState, insertResult)
uiBuilder.importSeedPhrase.updateSuggestions(mediateState, emptyList())
uiBuilder.importSeedPhrase.updateSuggestions(mediateState, persistentListOf())
}
}
}

View file

@ -9,6 +9,7 @@ plugins {
dependencies {
/** Core modules */
implementation(project(":core:analytics"))
implementation(projects.core.analytics.models)
implementation(project(":core:res"))
implementation(project(":core:utils"))
implementation(project(":core:ui"))

View file

@ -1,6 +1,6 @@
package com.tangem.feature.referral.analytics
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
sealed class ReferralEvents(event: String) : AnalyticsEvent(REFERRAL_PROGRAM_CATEGORY, event) {

View file

@ -10,6 +10,7 @@ plugins {
dependencies {
/** Core modules */
implementation(project(":core:analytics"))
implementation(projects.core.analytics.models)
implementation(project(":core:featuretoggles"))
implementation(project(":core:utils"))
implementation(project(":core:ui"))

View file

@ -1,6 +1,6 @@
package com.tangem.feature.swap.analytics
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
sealed class SwapEvents(
event: String,
@ -13,7 +13,6 @@ sealed class SwapEvents(
)
object SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked")
object ReceiveTokenClicked : SwapEvents(event = "Receive Token Clicked")
object ChooseTokenScreenOpened : SwapEvents(event = "Choose Token Screen Opened")
object SearchTokenClicked : SwapEvents(event = "Searched Token Clicked")
data class ButtonSwapClicked(val sendToken: String, val receiveToken: String) : SwapEvents(
@ -24,7 +23,6 @@ sealed class SwapEvents(
object ButtonGivePermissionClicked : SwapEvents(event = "Button - Give permission")
object ButtonPermissionApproveClicked : SwapEvents(event = "Button - Permission Approve")
object ButtonPermissionCancelClicked : SwapEvents(event = "Button - Permission Cancel")
object ButtonPermitAndSwapClicked : SwapEvents(event = "Button - Permit and Swap")
object ButtonSwipeClicked : SwapEvents(event = "Button - Swipe")
object SwapInProgressScreen : SwapEvents(event = "Swap in Progress Screen Opened")
}

View file

@ -24,6 +24,9 @@ dependencies {
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.coil)
implementation(deps.kotlin.immutable.collections)
/** DI */
implementation(deps.hilt.android)
@ -32,6 +35,7 @@ dependencies {
/** Core modules */
implementation(projects.core.featuretoggles)
implementation(projects.core.ui)
implementation(projects.core.navigation)
/** Feature Apis */
implementation(projects.features.tokendetails.api)

View file

@ -1,5 +1,6 @@
package com.tangem.feature.tokendetails.di
import com.tangem.core.navigation.NavigationStateHolder
import com.tangem.feature.tokendetails.presentation.router.DefaultTokenDetailsRouter
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import dagger.Module
@ -14,5 +15,7 @@ internal object TokenDetailsRouterModule {
@Provides
@ActivityScoped
fun provideTokenDetailsRouter(): TokenDetailsRouter = DefaultTokenDetailsRouter()
fun provideTokenDetailsRouter(navigationStateHolder: NavigationStateHolder): TokenDetailsRouter {
return DefaultTokenDetailsRouter(navigationStateHolder)
}
}

View file

@ -0,0 +1,46 @@
package com.tangem.feature.tokendetails.presentation
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.hilt.navigation.compose.hiltViewModel
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsViewModel
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class TokenDetailsFragment : Fragment() {
@Inject
lateinit var tokenDetailsRouter: TokenDetailsRouter
private val internalTokenDetailsRouter: InnerTokenDetailsRouter
get() = requireNotNull(tokenDetailsRouter as? InnerTokenDetailsRouter) {
"internalTokenDetailsRouter should be instance of InnerTokenDetailsRouter"
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(inflater.context).apply {
setContent {
TangemTheme {
val systemBarsColor = TangemTheme.colors.background.secondary
SystemBarsEffect {
setSystemBarsColor(systemBarsColor)
}
val viewModel = hiltViewModel<TokenDetailsViewModel>()
viewModel.router = this@TokenDetailsFragment.internalTokenDetailsRouter
TokenDetailsScreen(state = viewModel.uiState)
}
}
}
}
}

View file

@ -1,10 +1,17 @@
package com.tangem.feature.tokendetails.presentation.router
import androidx.fragment.app.Fragment
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.NavigationStateHolder
import com.tangem.feature.tokendetails.presentation.TokenDetailsFragment
internal class DefaultTokenDetailsRouter : TokenDetailsRouter {
internal class DefaultTokenDetailsRouter(
private val navigationStateHolder: NavigationStateHolder,
) : InnerTokenDetailsRouter {
// TODO: Doston fix it in next PRs
override fun getEntryFragment(): Fragment = Fragment()
override fun getEntryFragment(): Fragment = TokenDetailsFragment()
override fun popBackStack() {
navigationStateHolder.navigate(NavigationAction.PopBackTo())
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.feature.tokendetails.presentation.router
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
internal interface InnerTokenDetailsRouter : TokenDetailsRouter {
fun popBackStack()
}

View file

@ -0,0 +1,93 @@
package com.tangem.feature.tokendetails.presentation.tokendetails
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.marketprice.PriceChangeConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState
import com.tangem.features.tokendetails.impl.R
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
internal object TokenDetailsPreviewData {
val tokenDetailsTopAppBarConfig = TokenDetailsTopAppBarConfig(onBackClick = {}, onMoreClick = {})
val tokenInfoBlockStateWithLongNameInMainCurrency = TokenInfoBlockState(
name = "Stellar (XLM) with long name test",
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png",
currency = TokenInfoBlockState.Currency.Native,
)
val tokenInfoBlockStateWithLongName = TokenInfoBlockState(
name = "Tether (USDT) with long name test",
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png",
currency = TokenInfoBlockState.Currency.Token(
networkName = "ERC20",
networkIcon = R.drawable.img_eth_22,
blockchainName = "Ethereum",
),
)
val tokenInfoBlockState = TokenInfoBlockState(
name = "Tether USDT",
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png",
currency = TokenInfoBlockState.Currency.Token(
networkName = "ERC20",
networkIcon = R.drawable.img_eth_22,
blockchainName = "Ethereum",
),
)
private val actionButtons = persistentListOf(
ActionButtonConfig(
text = "Buy",
iconResId = R.drawable.ic_plus_24,
onClick = {},
),
ActionButtonConfig(
text = "Send",
iconResId = R.drawable.ic_arrow_up_24,
onClick = {},
),
ActionButtonConfig(
text = "Receive",
iconResId = R.drawable.ic_arrow_down_24,
onClick = {},
),
ActionButtonConfig(
text = "Exchange",
iconResId = R.drawable.ic_exchange_vertical_24,
onClick = {},
),
)
private val disabledActionButtons = actionButtons.map { it.copy(enabled = false) }.toPersistentList()
val balanceLoading = TokenDetailsBalanceBlockState.Loading(actionButtons = disabledActionButtons)
val balanceContent = TokenDetailsBalanceBlockState.Content(
actionButtons = actionButtons,
fiatBalance = "123,00$",
cryptoBalance = "866,96 USDT",
)
val balanceError = TokenDetailsBalanceBlockState.Error(actionButtons = disabledActionButtons)
val marketPriceContent = MarketPriceBlockState.Content(
currencyName = "USDT",
price = "98900 $",
priceChangeConfig = PriceChangeConfig(
valueInPercent = "10.89%",
type = PriceChangeConfig.Type.UP,
),
)
private val marketPriceLoading = MarketPriceBlockState.Loading(currencyName = "USDT")
val tokenDetailsState = TokenDetailsState(
topAppBarConfig = tokenDetailsTopAppBarConfig,
tokenInfoBlockState = tokenInfoBlockState,
tokenBalanceBlockState = balanceLoading,
marketPriceBlockState = marketPriceLoading,
)
}

View file

@ -0,0 +1,23 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import kotlinx.collections.immutable.ImmutableList
sealed class TokenDetailsBalanceBlockState {
abstract val actionButtons: ImmutableList<ActionButtonConfig>
data class Loading(
override val actionButtons: ImmutableList<ActionButtonConfig>,
) : TokenDetailsBalanceBlockState()
data class Content(
override val actionButtons: ImmutableList<ActionButtonConfig>,
val fiatBalance: String,
val cryptoBalance: String,
) : TokenDetailsBalanceBlockState()
data class Error(
override val actionButtons: ImmutableList<ActionButtonConfig>,
) : TokenDetailsBalanceBlockState()
}

View file

@ -0,0 +1,10 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
data class TokenDetailsState(
val topAppBarConfig: TokenDetailsTopAppBarConfig,
val tokenInfoBlockState: TokenInfoBlockState,
val tokenBalanceBlockState: TokenDetailsBalanceBlockState,
val marketPriceBlockState: MarketPriceBlockState,
)

View file

@ -0,0 +1,6 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
data class TokenDetailsTopAppBarConfig(
val onBackClick: () -> Unit,
val onMoreClick: () -> Unit,
)

View file

@ -0,0 +1,23 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import androidx.annotation.DrawableRes
data class TokenInfoBlockState(
val name: String,
val iconUrl: String,
val currency: Currency,
) {
sealed class Currency {
object Native : Currency()
/**
* @param networkName - token standard. Samples: ERC20, BEP20, BEP2, TRC20 and etc.
* @param blockchainName - token's blockchain name. Ethereum, Tron and etc.
*/
data class Token(
val networkName: String,
val blockchainName: String,
@DrawableRes val networkIcon: Int,
) : Currency()
}
}

View file

@ -0,0 +1,53 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.marketprice.MarketPriceBlock
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock
@Composable
internal fun TokenDetailsScreen(state: TokenDetailsState) {
Scaffold(
topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) },
containerColor = TangemTheme.colors.background.secondary,
) { scaffoldPaddings ->
Column(
modifier = Modifier
.padding(paddingValues = scaffoldPaddings)
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
TokenInfoBlock(state = state.tokenInfoBlockState)
TokenDetailsBalanceBlock(state = state.tokenBalanceBlockState)
MarketPriceBlock(state = state.marketPriceBlockState)
}
}
}
@Preview
@Composable
private fun Preview_TokenDetailsScreen_LightTheme() {
TangemTheme(isDark = false) {
TokenDetailsScreen(state = TokenDetailsPreviewData.tokenDetailsState)
}
}
@Preview
@Composable
private fun Preview_TokenDetailsScreen_DarkTheme() {
TangemTheme(isDark = true) {
TokenDetailsScreen(state = TokenDetailsPreviewData.tokenDetailsState)
}
}

View file

@ -0,0 +1,136 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.buttons.HorizontalActionChips
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
import com.tangem.features.tokendetails.impl.R
@Composable
internal fun TokenDetailsBalanceBlock(state: TokenDetailsBalanceBlockState, modifier: Modifier = Modifier) {
Surface(
modifier = modifier.fillMaxWidth(),
shape = TangemTheme.shapes.roundedCornersXMedium,
color = TangemTheme.colors.background.primary,
) {
Column {
Text(
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing12,
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
),
text = stringResource(id = R.string.onboarding_balance_title),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
maxLines = 1,
)
FiatBalance(
state = state,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing4)
.padding(horizontal = TangemTheme.dimens.spacing12),
)
CryptoBalance(
state = state,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing4)
.padding(horizontal = TangemTheme.dimens.spacing12),
)
HorizontalActionChips(
buttons = state.actionButtons,
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing12),
)
}
}
}
@Composable
private fun FiatBalance(state: TokenDetailsBalanceBlockState, modifier: Modifier = Modifier) {
when (state) {
is TokenDetailsBalanceBlockState.Loading -> RectangleShimmer(
modifier = modifier.size(
width = TangemTheme.dimens.size102,
height = TangemTheme.dimens.size24,
),
)
is TokenDetailsBalanceBlockState.Content -> Text(
modifier = modifier,
text = state.fiatBalance,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
is TokenDetailsBalanceBlockState.Error -> Text(
modifier = modifier,
text = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
}
}
@Composable
private fun CryptoBalance(state: TokenDetailsBalanceBlockState, modifier: Modifier = Modifier) {
when (state) {
is TokenDetailsBalanceBlockState.Loading -> RectangleShimmer(
modifier = modifier.size(
width = TangemTheme.dimens.size70,
height = TangemTheme.dimens.size16,
),
)
is TokenDetailsBalanceBlockState.Content -> Text(
modifier = modifier,
text = state.cryptoBalance,
style = TangemTheme.typography.caption,
color = TangemTheme.colors.text.primary1,
)
is TokenDetailsBalanceBlockState.Error -> Text(
modifier = modifier,
text = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
style = TangemTheme.typography.caption,
color = TangemTheme.colors.text.primary1,
)
}
}
@Preview
@Composable
private fun Preview_TokenDetailsBalanceBlock_LightTheme(
@PreviewParameter(TokenDetailsBalanceBlockStateProvider::class) state: TokenDetailsBalanceBlockState,
) {
TangemTheme(isDark = false) {
TokenDetailsBalanceBlock(state)
}
}
@Preview
@Composable
private fun Preview_TokenDetailsBalanceBlock_DarkTheme(
@PreviewParameter(TokenDetailsBalanceBlockStateProvider::class) state: TokenDetailsBalanceBlockState,
) {
TangemTheme(isDark = true) {
TokenDetailsBalanceBlock(state)
}
}
private class TokenDetailsBalanceBlockStateProvider : CollectionPreviewParameterProvider<TokenDetailsBalanceBlockState>(
collection = listOf(
TokenDetailsPreviewData.balanceLoading,
TokenDetailsPreviewData.balanceContent,
TokenDetailsPreviewData.balanceError,
),
)

View file

@ -0,0 +1,58 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.features.tokendetails.impl.R
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) {
TopAppBar(
navigationIcon = {
IconButton(onClick = config.onBackClick) {
Icon(
painter = painterResource(id = R.drawable.ic_back_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = "Back",
)
}
},
title = {},
actions = {
IconButton(onClick = config.onMoreClick) {
Icon(
painter = painterResource(id = R.drawable.ic_more_vertical_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = "More",
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = TangemTheme.colors.background.secondary,
titleContentColor = TangemTheme.colors.icon.primary1,
actionIconContentColor = TangemTheme.colors.icon.primary1,
),
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
)
}
@Preview
@Composable
private fun Preview_TokenDetailsTopAppBar_LightTheme() {
TangemTheme(isDark = false) {
TokenDetailsTopAppBar(config = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig)
}
}
@Preview
@Composable
private fun Preview_TokenDetailsTopAppBar_DarkTheme() {
TangemTheme(isDark = true) {
TokenDetailsTopAppBar(config = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig)
}
}

View file

@ -0,0 +1,141 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import coil.compose.rememberAsyncImagePainter
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState
import com.tangem.features.tokendetails.impl.R
@Composable
internal fun TokenInfoBlock(state: TokenInfoBlockState, modifier: Modifier = Modifier) {
Row(modifier = modifier.fillMaxWidth()) {
Column(
modifier = Modifier.weight(1F),
) {
Text(
text = state.name,
style = TangemTheme.typography.h1,
color = TangemTheme.colors.text.primary1,
)
NetworkInfoText(state.currency)
}
val tokenIconPainter = when (LocalInspectionMode.current) {
// show drawable res in preview
true -> painterResource(id = R.drawable.img_stellar_22)
false -> rememberAsyncImagePainter(model = state.iconUrl)
}
Image(
modifier = Modifier.size(TangemTheme.dimens.size48),
painter = tokenIconPainter,
contentDescription = null,
)
}
}
@Composable
private fun NetworkInfoText(currency: TokenInfoBlockState.Currency) {
when (currency) {
TokenInfoBlockState.Currency.Native -> {
Text(
text = stringResource(id = R.string.common_main_network),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption,
)
}
is TokenInfoBlockState.Currency.Token -> {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
) {
val state = extractNetwork(tokenCurrency = currency)
Text(
text = state.normalText,
style = TangemTheme.typography.caption,
color = TangemTheme.colors.text.tertiary,
)
Icon(
modifier = Modifier.size(TangemTheme.dimens.size16),
painter = painterResource(id = currency.networkIcon),
tint = Color.Unspecified,
contentDescription = null,
)
Text(
text = state.boldText,
style = TangemTheme.typography.caption.copy(fontWeight = FontWeight.Medium),
color = TangemTheme.colors.text.primary1,
)
}
}
}
}
private const val SEPARATOR = " %image% "
@Composable
private fun extractNetwork(tokenCurrency: TokenInfoBlockState.Currency.Token): ExtractedTokenNetworkText {
val splitString = stringResource(
id = R.string.token_details_token_type_subtitle,
formatArgs = arrayOf(
tokenCurrency.networkName,
tokenCurrency.blockchainName,
),
).split(SEPARATOR)
return remember(splitString) {
ExtractedTokenNetworkText(
normalText = splitString.firstOrNull().orEmpty(),
boldText = splitString.getOrNull(1).orEmpty(),
)
}
}
private data class ExtractedTokenNetworkText(val normalText: String, val boldText: String)
@Preview
@Composable
private fun Preview_TokenInfoBlock_LightTheme(
@PreviewParameter(TokenInfoStateProvider::class)
state: TokenInfoBlockState,
) {
TangemTheme(isDark = false) {
TokenInfoBlock(state, Modifier.background(TangemTheme.colors.background.secondary))
}
}
@Preview
@Composable
private fun Preview_TokenInfoBlock_DarkTheme(
@PreviewParameter(TokenInfoStateProvider::class)
state: TokenInfoBlockState,
) {
TangemTheme(isDark = true) {
TokenInfoBlock(state, Modifier.background(TangemTheme.colors.background.secondary))
}
}
private class TokenInfoStateProvider : CollectionPreviewParameterProvider<TokenInfoBlockState>(
collection = listOf(
TokenDetailsPreviewData.tokenInfoBlockState,
TokenDetailsPreviewData.tokenInfoBlockStateWithLongName,
TokenDetailsPreviewData.tokenInfoBlockStateWithLongNameInMainCurrency,
),
)

View file

@ -0,0 +1,46 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.properties.Delegates
private const val LOADING_DELAY = 4_000L
@HiltViewModel
internal class TokenDetailsViewModel @Inject constructor() : ViewModel() {
var router: InnerTokenDetailsRouter by Delegates.notNull()
var uiState by mutableStateOf(getInitialState())
private set
init {
// simulate loading state
viewModelScope.launch {
delay(LOADING_DELAY)
uiState = uiState.copy(
tokenBalanceBlockState = TokenDetailsPreviewData.balanceContent,
marketPriceBlockState = TokenDetailsPreviewData.marketPriceContent,
)
}
}
private fun getInitialState() = TokenDetailsPreviewData.tokenDetailsState.copy(
topAppBarConfig = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig.copy(
onBackClick = ::onBackClick,
),
)
private fun onBackClick() {
router.popBackStack()
}
}

View file

@ -26,8 +26,11 @@ dependencies {
implementation(deps.compose.reorderable)
/** Other libraries */
implementation(deps.arrow.core)
implementation(deps.kotlin.immutable.collections)
implementation(deps.tangem.card.core)
implementation(deps.tangem.blockchain)
implementation(deps.arrow.core)
/** DI */
implementation(deps.hilt.android)
@ -37,14 +40,17 @@ dependencies {
implementation(project(":core:featuretoggles"))
implementation(project(":core:navigation"))
implementation(project(":core:ui"))
implementation(projects.core.utils)
/** Feature Apis */
implementation(project(":features:wallet:api"))
/** Domain modules */
implementation(project(":common"))
implementation(projects.domain.card)
implementation(project(":domain:legacy"))
implementation(project(":domain:models"))
implementation(project(":domain:wallets"))
implementation(project(":domain:wallets:models"))
implementation(projects.domain.tokens)
}

View file

@ -1,8 +1,10 @@
package com.tangem.feature.wallet.presentation.common
import com.tangem.core.ui.R
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.marketprice.PriceChangeConfig
import com.tangem.core.ui.components.transactions.TransactionState
import com.tangem.feature.wallet.presentation.common.state.PriceChangeConfig
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState
import com.tangem.feature.wallet.presentation.organizetokens.DraggableItem
@ -18,7 +20,7 @@ internal object WalletPreviewData {
val walletTopBarConfig = WalletTopBarConfig(onScanCardClick = {}, onMoreClick = {})
val walletCardContentState = WalletCardState.Content(
id = UUID.randomUUID().toString(),
id = UserWalletId(UUID.randomUUID().toString()),
title = "Wallet 1",
balance = "8923,05 $",
additionalInfo = "3 cards • Seed enabled",
@ -27,7 +29,7 @@ internal object WalletPreviewData {
)
val walletCardLoadingState = WalletCardState.Loading(
id = UUID.randomUUID().toString(),
id = UserWalletId(UUID.randomUUID().toString()),
title = "Wallet 1",
additionalInfo = "3 cards • Seed enabled",
imageResId = R.drawable.ill_businessman_3d,
@ -35,7 +37,7 @@ internal object WalletPreviewData {
)
val walletCardHiddenContentState = WalletCardState.HiddenContent(
id = UUID.randomUUID().toString(),
id = UserWalletId(UUID.randomUUID().toString()),
title = "Wallet 1",
additionalInfo = "3 cards • Seed enabled",
imageResId = R.drawable.ill_businessman_3d,
@ -43,21 +45,23 @@ internal object WalletPreviewData {
)
val walletCardErrorState = WalletCardState.Error(
id = UUID.randomUUID().toString(),
id = UserWalletId(UUID.randomUUID().toString()),
title = "Wallet 1",
additionalInfo = "3 cards • Seed enabled",
imageResId = R.drawable.ill_businessman_3d,
onClick = null,
)
val wallets = mapOf(
UserWalletId(stringValue = "123") to walletCardContentState,
UserWalletId(stringValue = "321") to walletCardLoadingState,
UserWalletId(stringValue = "42") to walletCardHiddenContentState,
UserWalletId(stringValue = "24") to walletCardErrorState,
)
val walletListConfig = WalletsListConfig(
selectedWalletIndex = 0,
wallets = persistentListOf(
walletCardContentState,
walletCardLoadingState,
walletCardHiddenContentState,
walletCardErrorState,
),
wallets = wallets.values.toPersistentList(),
onWalletChange = {},
)
@ -196,7 +200,16 @@ internal object WalletPreviewData {
),
)
val manageButtons = persistentListOf(
val bottomSheet = WalletBottomSheetConfig(
isShow = false,
onDismissRequest = {},
content = WalletBottomSheetConfig.BottomSheetContentConfig.UnlockWallets(
onUnlockClick = {},
onScanClick = {},
),
)
private val manageButtons = persistentListOf(
WalletManageButton.Buy(onClick = {}),
WalletManageButton.Send(onClick = {}),
WalletManageButton.Receive(onClick = {}),
@ -204,15 +217,6 @@ internal object WalletPreviewData {
WalletManageButton.CopyAddress(onClick = {}),
)
val marketplaceBlockContent = WalletMarketplaceBlockState.Content(
currencyName = "BTC",
price = "0.11$",
priceChangeConfig = PriceChangeConfig(
valueInPercent = "5.16%",
type = PriceChangeConfig.Type.UP,
),
)
val multicurrencyWalletScreenState = WalletStateHolder.MultiCurrencyContent(
onBackClick = {},
topBarConfig = walletTopBarConfig,
@ -266,12 +270,17 @@ internal object WalletPreviewData {
),
),
),
pullToRefreshConfig = WalletPullToRefreshConfig(
isRefreshing = false,
onRefresh = {},
),
notifications = persistentListOf(
WalletNotification.UnreachableNetworks,
WalletNotification.LikeTangemApp(onClick = {}),
WalletNotification.NeedToBackup(onClick = {}),
WalletNotification.BackupCard(onClick = {}),
WalletNotification.ScanCard(onClick = {}),
),
bottomSheet = bottomSheet,
onOrganizeTokensClick = {},
)
@ -298,8 +307,20 @@ internal object WalletPreviewData {
),
),
),
pullToRefreshConfig = WalletPullToRefreshConfig(
isRefreshing = false,
onRefresh = {},
),
notifications = persistentListOf(WalletNotification.LikeTangemApp(onClick = {})),
buttons = manageButtons,
marketplaceBlockState = marketplaceBlockContent,
buttons = manageButtons.map(WalletManageButton::config).toPersistentList(),
bottomSheet = bottomSheet,
marketPriceBlockState = MarketPriceBlockState.Content(
currencyName = "BTC",
price = "98900.12$",
priceChangeConfig = PriceChangeConfig(
valueInPercent = "5.16%",
type = PriceChangeConfig.Type.UP,
),
),
)
}

View file

@ -28,11 +28,11 @@ import androidx.constraintlayout.compose.Dimension
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.marketprice.PriceChangeConfig
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemTypography
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.common.state.PriceChangeConfig
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState
import org.burnoutcrew.reorderable.ReorderableLazyListState

View file

@ -1,15 +0,0 @@
package com.tangem.feature.wallet.presentation.common.state
/**
* Price changing config
*
* @property valueInPercent value in percent
* @property type type [Type]
*/
internal data class PriceChangeConfig(val valueInPercent: String, val type: Type) {
/** Price changing type */
enum class Type {
UP, DOWN
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.common.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.marketprice.PriceChangeConfig
/** Token item state */
@Immutable

View file

@ -4,6 +4,7 @@ import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.hilt.navigation.compose.hiltViewModel
@ -41,6 +42,8 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation
) {
composable(WalletScreens.WALLET.name) {
val viewModel = hiltViewModel<WalletViewModel>().apply { router = this@DefaultWalletRouter }
LocalLifecycleOwner.current.lifecycle.addObserver(observer = viewModel)
WalletScreen(state = viewModel.uiState)
}

View file

@ -0,0 +1,79 @@
package com.tangem.feature.wallet.presentation.wallet.state
import androidx.annotation.DrawableRes
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.WrappedList
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.feature.wallet.impl.R
/**
* Wallet bottom sheet config
*
* @property isShow flag that determine if bottom sheet is shown
* @property onDismissRequest lambda be invoked when bottom sheet is dismissed
* @property content content config
*
[REDACTED_AUTHOR]
*/
// TODO: Finalize notification strings [REDACTED_JIRA]
internal data class WalletBottomSheetConfig(
val isShow: Boolean,
val onDismissRequest: () -> Unit,
val content: BottomSheetContentConfig,
) {
sealed class BottomSheetContentConfig(
open val title: TextReference,
open val subtitle: TextReference,
@DrawableRes open val iconResId: Int,
open val tint: Color? = null,
val primaryButtonConfig: ButtonConfig,
val secondaryButtonConfig: ButtonConfig? = null,
) {
data class ButtonConfig(val text: String, val onClick: () -> Unit, @DrawableRes val iconResId: Int? = null)
data class UnlockWallets(
val onUnlockClick: () -> Unit,
val onScanClick: () -> Unit,
) : BottomSheetContentConfig(
title = TextReference.Str(value = "Unlock needed"),
subtitle = TextReference.Str(
value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " +
"incididunt ut labore et dolore magna aliqua.",
),
iconResId = R.drawable.ic_locked_24,
tint = TangemColorPalette.Black,
primaryButtonConfig = ButtonConfig(text = "Unlock", onClick = onUnlockClick),
secondaryButtonConfig = ButtonConfig(
text = "Scan card",
onClick = onScanClick,
iconResId = R.drawable.ic_tangem_24,
),
)
data class LikeTangemApp(
val onRateTheAppClick: () -> Unit,
val onShareClick: () -> Unit,
) : BottomSheetContentConfig(
title = TextReference.Str(value = "Like Tangem App?"),
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),
)
data class MultiWalletAlreadySignedHashes(val onLearnClick: () -> 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),
iconResId = R.drawable.img_attention_20,
tint = null,
primaryButtonConfig = ButtonConfig(text = "Learn more", onClick = onLearnClick),
)
}
}

View file

@ -2,13 +2,14 @@ package com.tangem.feature.wallet.presentation.wallet.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.domain.wallets.models.UserWalletId
/** Wallet card state */
@Immutable
internal sealed interface WalletCardState {
/** Id */
val id: String
val id: UserWalletId
/** Title */
val title: String
@ -34,7 +35,7 @@ internal sealed interface WalletCardState {
* @property balance wallet balance
*/
data class Content(
override val id: String,
override val id: UserWalletId,
override val title: String,
override val additionalInfo: String,
override val imageResId: Int?,
@ -52,7 +53,7 @@ internal sealed interface WalletCardState {
* @property onClick lambda be invoked when wallet card is clicked
*/
data class Loading(
override val id: String,
override val id: UserWalletId,
override val title: String,
override val additionalInfo: String,
override val imageResId: Int?,
@ -69,7 +70,7 @@ internal sealed interface WalletCardState {
* @property onClick lambda be invoked when wallet card is clicked
*/
data class HiddenContent(
override val id: String,
override val id: UserWalletId,
override val title: String,
override val additionalInfo: String,
override val imageResId: Int?,
@ -86,7 +87,7 @@ internal sealed interface WalletCardState {
* @property onClick lambda be invoked when wallet card is clicked
*/
data class Error(
override val id: String,
override val id: UserWalletId,
override val title: String,
override val additionalInfo: String,
override val imageResId: Int?,

View file

@ -1,33 +0,0 @@
package com.tangem.feature.wallet.presentation.wallet.state
import com.tangem.feature.wallet.presentation.common.state.PriceChangeConfig
/**
* Wallet marketplace component state
*
* @property currencyName currency name
*
[REDACTED_AUTHOR]
*/
internal sealed class WalletMarketplaceBlockState(open val currencyName: String) {
/**
* Loading state
*
* @property currencyName currency name
*/
data class Loading(override val currencyName: String) : WalletMarketplaceBlockState(currencyName = currencyName)
/**
* Content state
*
* @property currencyName currency name
* @property price price
* @property priceChangeConfig price change config
*/
data class Content(
override val currencyName: String,
val price: String,
val priceChangeConfig: PriceChangeConfig,
) : WalletMarketplaceBlockState(currencyName = currencyName)
}

View file

@ -1,6 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.state
import com.tangem.core.ui.components.notifications.NotificationState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.WrappedList
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.feature.wallet.impl.R
@ -11,26 +13,107 @@ import com.tangem.feature.wallet.impl.R
*
[REDACTED_AUTHOR]
*/
// TODO: Finalize notification strings [REDACTED_JIRA]
sealed class WalletNotification(open val state: NotificationState) {
/** Clickable notification */
sealed interface Clickable {
/** Lambda be invoked when notification is clicked */
val onClick: () -> Unit
}
/** "Development card" notification */
object DevCard : WalletNotification(
state = NotificationState.Simple(
title = TextReference.Res(id = R.string.common_warning),
subtitle = TextReference.Res(id = R.string.alert_developer_card),
iconResId = R.drawable.ic_alert_circle_24,
tint = TangemColorPalette.Amaranth,
),
)
/** "Test card" notification */
object TestCard : WalletNotification(
state = NotificationState.Simple(
title = TextReference.Res(id = R.string.common_warning),
subtitle = TextReference.Res(id = R.string.warning_testnet_card_message),
iconResId = R.drawable.ic_alert_circle_24,
tint = TangemColorPalette.Amaranth,
),
)
/** "Demo card" notification */
object DemoCard : WalletNotification(
state = NotificationState.Simple(
title = TextReference.Res(id = R.string.common_warning),
subtitle = TextReference.Res(id = R.string.alert_demo_message),
iconResId = R.drawable.ic_alert_circle_24,
tint = TangemColorPalette.Amaranth,
),
)
/** "Card verification failed" notification */
object CardVerificationFailed : WalletNotification(
state = NotificationState.Simple(
title = TextReference.Res(id = R.string.warning_failed_to_verify_card_title),
subtitle = TextReference.Res(id = R.string.warning_failed_to_verify_card_message),
iconResId = R.drawable.ic_alert_circle_24,
tint = TangemColorPalette.Amaranth,
),
)
/**
* "Wallet already signed hashes" notification
*
* @property onClick lambda be invoked when notification is clicked
*/
data class WalletAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification(
state = NotificationState.Clickable(
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,
),
)
/**
* "Multi wallet already signed hashes" notification
*
* @property onClick lambda be invoked when notification is clicked
*/
data class MultiWalletAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification(
state = NotificationState.Clickable(
title = TextReference.Res(
id = R.string.warning_important_security_info,
formatArgs = WrappedList(listOf("\u26A0")),
),
subtitle = TextReference.Res(id = R.string.warning_signed_tx_previously),
iconResId = R.drawable.img_attention_20,
onClick = onClick,
tint = null,
),
)
/**
* "Backup the card" notification
*
* @property onClick lambda be invoked when notification is clicked
*/
data class NeedToBackup(val onClick: () -> Unit) : WalletNotification(
state = NotificationState.Action(
title = "Backup your card",
iconResId = R.drawable.ic_alert_circle_24,
data class BackupCard(override val onClick: () -> Unit) : Clickable, WalletNotification(
state = NotificationState.Clickable(
title = TextReference.Str(value = "Backup your card"),
iconResId = R.drawable.img_attention_20,
onClick = onClick,
tint = TangemColorPalette.Amaranth,
tint = null,
),
)
/** "Unreachable networks" notification */
object UnreachableNetworks : WalletNotification(
state = NotificationState.Simple(
title = "Some networks are unreachable",
title = TextReference.Str(value = "Some networks are unreachable"),
iconResId = R.drawable.img_attention_20,
tint = null,
),
@ -41,9 +124,9 @@ sealed class WalletNotification(open val state: NotificationState) {
*
* @property onClick lambda be invoked when notification is clicked
*/
data class LikeTangemApp(val onClick: () -> Unit) : WalletNotification(
state = NotificationState.Action(
title = "Like Tangem App?",
data class LikeTangemApp(override val onClick: () -> Unit) : Clickable, WalletNotification(
state = NotificationState.Clickable(
title = TextReference.Str(value = "Like Tangem App?"),
iconResId = R.drawable.ic_star_24,
onClick = onClick,
tint = TangemColorPalette.Tangerine,
@ -55,11 +138,24 @@ sealed class WalletNotification(open val state: NotificationState) {
*
* @property onClick lambda be invoked when notification is clicked
*/
data class ScanCard(val onClick: () -> Unit) : WalletNotification(
state = NotificationState.Action(
title = "Scan your card to continue",
data class ScanCard(override val onClick: () -> Unit) : Clickable, WalletNotification(
state = NotificationState.Clickable(
title = TextReference.Str(value = "Scan your card to continue"),
iconResId = R.drawable.ic_tangem_24,
onClick = onClick,
),
)
/**
* "Unlock wallets" notification
*
* @property onClick lambda be invoked when notification is clicked
*/
data class UnlockWallets(override val onClick: () -> Unit) : Clickable, WalletNotification(
state = NotificationState.Clickable(
title = TextReference.Str(value = "Unlock needed"),
iconResId = R.drawable.ic_locked_24,
onClick = onClick,
),
)
}

View file

@ -0,0 +1,9 @@
package com.tangem.feature.wallet.presentation.wallet.state
/**
* Wallet screen top bar config
*
* @property isRefreshing state is indicator visible
* @property onRefresh lambda be invoked when pulled to refresh
*/
data class WalletPullToRefreshConfig(val isRefreshing: Boolean, val onRefresh: () -> Unit)

View file

@ -1,15 +1,20 @@
package com.tangem.feature.wallet.presentation.wallet.state
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* Wallet screen state holder
*
* @property onBackClick lambda be invoked when back button is clicked
* @property topBarConfig top bar config
* @property walletsListConfig wallets list config
* @property contentItems content items
* @property notifications notifications
* @property onBackClick lambda be invoked when back button is clicked
* @property topBarConfig top bar config
* @property walletsListConfig wallets list config
* @property pullToRefreshConfig pull to refresh config
* @property contentItems content items
* @property notifications notifications
*
[REDACTED_AUTHOR]
*/
@ -17,47 +22,172 @@ internal sealed class WalletStateHolder(
open val onBackClick: () -> Unit,
open val topBarConfig: WalletTopBarConfig,
open val walletsListConfig: WalletsListConfig,
open val pullToRefreshConfig: WalletPullToRefreshConfig,
open val contentItems: ImmutableList<WalletContentItemState>,
open val notifications: ImmutableList<WalletNotification>,
open val bottomSheet: WalletBottomSheetConfig? = null,
) {
fun copySealed(
onBackClick: () -> Unit = this.onBackClick,
topBarConfig: WalletTopBarConfig = this.topBarConfig,
walletsListConfig: WalletsListConfig = this.walletsListConfig,
pullToRefreshConfig: WalletPullToRefreshConfig = this.pullToRefreshConfig,
contentItems: ImmutableList<WalletContentItemState> = this.contentItems,
notifications: ImmutableList<WalletNotification> = this.notifications,
bottomSheet: WalletBottomSheetConfig? = this.bottomSheet,
): WalletStateHolder {
return when (this) {
is MultiCurrencyContent -> this.copy(
onBackClick = onBackClick,
topBarConfig = topBarConfig,
walletsListConfig = walletsListConfig,
pullToRefreshConfig = pullToRefreshConfig,
contentItems = contentItems as ImmutableList<WalletContentItemState.MultiCurrencyItem>,
notifications = notifications,
bottomSheet = bottomSheet,
)
is SingleCurrencyContent -> this.copy(
onBackClick = onBackClick,
topBarConfig = topBarConfig,
walletsListConfig = walletsListConfig,
pullToRefreshConfig = pullToRefreshConfig,
contentItems = contentItems as ImmutableList<WalletContentItemState.SingleCurrencyItem>,
notifications = notifications,
bottomSheet = bottomSheet,
)
is UnlockWalletContent -> this.copy(
onBackClick = onBackClick,
topBarConfig = topBarConfig,
walletsListConfig = walletsListConfig,
pullToRefreshConfig = pullToRefreshConfig,
)
is Loading -> copy(onBackClick = onBackClick)
}
}
/**
* Multi currency wallet content state
*
* @property onBackClick lambda be invoked when back button is clicked
* @property topBarConfig top bar config
* @property walletsListConfig wallets list config
* @property contentItems content items
* @property notifications notifications
* @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked
* @property onBackClick lambda be invoked when back button is clicked
* @property topBarConfig top bar config
* @property walletsListConfig wallets list config
* @property pullToRefreshConfig pull to refresh config
* @property contentItems content items
* @property notifications notifications
* @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked
*/
data class MultiCurrencyContent(
override val onBackClick: () -> Unit,
override val topBarConfig: WalletTopBarConfig,
override val walletsListConfig: WalletsListConfig,
override val pullToRefreshConfig: WalletPullToRefreshConfig,
override val contentItems: ImmutableList<WalletContentItemState.MultiCurrencyItem>,
override val notifications: ImmutableList<WalletNotification>,
override val bottomSheet: WalletBottomSheetConfig? = null,
val onOrganizeTokensClick: () -> Unit,
) : WalletStateHolder(onBackClick, topBarConfig, walletsListConfig, contentItems, notifications)
) : WalletStateHolder(
onBackClick = onBackClick,
topBarConfig = topBarConfig,
walletsListConfig = walletsListConfig,
pullToRefreshConfig = pullToRefreshConfig,
contentItems = contentItems,
bottomSheet = bottomSheet,
notifications = notifications,
)
/**
* Single currency wallet content state
*
* @property onBackClick lambda be invoked when back button is clicked
* @property topBarConfig top bar config
* @property walletsListConfig wallets list config
* @property contentItems content items
* @property notifications notifications
* @property buttons manage buttons
* @property marketplaceBlockState marketplace block state
* @property onBackClick lambda be invoked when back button is clicked
* @property topBarConfig top bar config
* @property walletsListConfig wallets list config
* @property pullToRefreshConfig pull to refresh config
* @property contentItems content items
* @property notifications notifications
* @property buttons manage buttons
* @property marketPriceBlockState market price block state
*/
data class SingleCurrencyContent(
override val onBackClick: () -> Unit,
override val topBarConfig: WalletTopBarConfig,
override val walletsListConfig: WalletsListConfig,
override val pullToRefreshConfig: WalletPullToRefreshConfig,
override val contentItems: ImmutableList<WalletContentItemState.SingleCurrencyItem>,
override val notifications: ImmutableList<WalletNotification>,
val buttons: ImmutableList<WalletManageButton>,
val marketplaceBlockState: WalletMarketplaceBlockState,
) : WalletStateHolder(onBackClick, topBarConfig, walletsListConfig, contentItems, notifications)
override val bottomSheet: WalletBottomSheetConfig? = null,
val buttons: ImmutableList<ActionButtonConfig>,
val marketPriceBlockState: MarketPriceBlockState,
) : WalletStateHolder(
onBackClick = onBackClick,
topBarConfig = topBarConfig,
walletsListConfig = walletsListConfig,
pullToRefreshConfig = pullToRefreshConfig,
contentItems = contentItems,
notifications = notifications,
bottomSheet = bottomSheet,
)
/**
* Unlock wallet content state
*
* @property onBackClick lambda be invoked when back button is clicked
* @property topBarConfig top bar config
* @property walletsListConfig wallets list config
* @property onUnlockWalletsNotificationClick lambda be invoked when unlock wallets notification is clicked
* @property onBottomSheetDismissRequest lambda be invoked when bottom sheet is dismissed
* @property onUnlockClick lambda be invoked when unlock button is clicked
* @property onScanClick lambda be invoked when scan card button is clicked
*/
data class UnlockWalletContent(
override val onBackClick: () -> Unit,
override val topBarConfig: WalletTopBarConfig,
override val walletsListConfig: WalletsListConfig,
override val pullToRefreshConfig: WalletPullToRefreshConfig,
val onUnlockWalletsNotificationClick: () -> Unit,
val onBottomSheetDismissRequest: () -> Unit,
val onUnlockClick: () -> Unit,
val onScanClick: () -> Unit,
) : WalletStateHolder(
onBackClick = onBackClick,
topBarConfig = topBarConfig,
walletsListConfig = walletsListConfig,
pullToRefreshConfig = pullToRefreshConfig,
contentItems = persistentListOf(WalletContentItemState.Loading),
notifications = persistentListOf(WalletNotification.UnlockWallets(onUnlockWalletsNotificationClick)),
bottomSheet = WalletBottomSheetConfig(
isShow = false,
onDismissRequest = onBottomSheetDismissRequest,
content = WalletBottomSheetConfig.BottomSheetContentConfig.UnlockWallets(
onUnlockClick = onUnlockClick,
onScanClick = onScanClick,
),
),
)
/**
* Loading state
*
* @property onBackClick lambda be invoked when back button is clicked
*/
data class Loading(override val onBackClick: () -> Unit) : WalletStateHolder(
onBackClick = onBackClick,
topBarConfig = WalletTopBarConfig(onScanCardClick = {}, onMoreClick = {}),
walletsListConfig = WalletsListConfig(
selectedWalletIndex = 0,
wallets = persistentListOf(
WalletCardState.Loading(
id = UserWalletId(stringValue = ""),
title = "",
additionalInfo = "",
imageResId = null,
),
),
onWalletChange = {},
),
pullToRefreshConfig = WalletPullToRefreshConfig(isRefreshing = false, onRefresh = {}),
contentItems = persistentListOf(WalletContentItemState.Loading),
notifications = persistentListOf(),
bottomSheet = null,
)
}

View file

@ -0,0 +1,109 @@
package com.tangem.feature.wallet.presentation.wallet.state.builder
import com.tangem.common.Provider
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 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,
) {
fun getInitialState(): WalletStateHolder = WalletStateHolder.Loading(onBackClick = ::onBackClick)
fun getContentState(wallets: List<UserWallet>): WalletStateHolder {
val cardTypeResolver = requireNotNull(wallets.firstOrNull()).scanResponse.cardTypesResolver
return if (cardTypeResolver.isMultiwalletAllowed()) {
createMultiCurrencyState(wallets)
} else {
createSingleCurrencyState(wallets)
}
}
private fun createMultiCurrencyState(wallets: List<UserWallet>): WalletStateHolder.MultiCurrencyContent {
return WalletStateHolder.MultiCurrencyContent(
onBackClick = ::onBackClick,
topBarConfig = createTopBarConfig(),
walletsListConfig = createWalletsListConfig(wallets),
pullToRefreshConfig = createPullToRefreshConfig(),
contentItems = persistentListOf(),
notifications = persistentListOf(), // TODO: create notifications
bottomSheet = WalletBottomSheetConfig(
// TODO: check notifications
isShow = false,
onDismissRequest = {},
content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp(
onRateTheAppClick = {},
onShareClick = {},
),
),
onOrganizeTokensClick = routerProvider()::openOrganizeTokensScreen,
)
}
private fun createSingleCurrencyState(wallets: List<UserWallet>): WalletStateHolder.SingleCurrencyContent {
return WalletStateHolder.SingleCurrencyContent(
onBackClick = ::onBackClick,
topBarConfig = createTopBarConfig(),
walletsListConfig = createWalletsListConfig(wallets),
pullToRefreshConfig = createPullToRefreshConfig(),
contentItems = persistentListOf(),
notifications = persistentListOf(), // TODO: create notifications
bottomSheet = WalletBottomSheetConfig(
// TODO: check notifications
isShow = false,
onDismissRequest = {},
content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp(
onRateTheAppClick = {},
onShareClick = {},
),
),
buttons = WalletPreviewData.singleWalletScreenState.buttons, // TODO: create buttons
// TODO: create market price block
marketPriceBlockState = WalletPreviewData.singleWalletScreenState.marketPriceBlockState,
)
}
private fun onBackClick() = routerProvider().popBackStack()
private fun createTopBarConfig(): WalletTopBarConfig {
return WalletTopBarConfig(
onScanCardClick = onScanCardClick,
onMoreClick = routerProvider()::openDetailsScreen,
)
}
private fun createWalletsListConfig(wallets: List<UserWallet>): WalletsListConfig {
return WalletsListConfig(
selectedWalletIndex = 0,
wallets = wallets.map { wallet ->
WalletCardState.Loading(
id = if (wallet.scanResponse.cardTypesResolver.isMultiwalletAllowed()) { // TODO
UserWalletId("123")
} else {
UserWalletId("321")
},
// TODO: wallet.walletId,
title = wallet.name,
additionalInfo = "", // TODO
imageResId = WalletImageResolver.resolve(cardTypesResolver = wallet.scanResponse.cardTypesResolver),
)
}.toImmutableList(),
onWalletChange = onWalletChange,
)
}
private fun createPullToRefreshConfig(): WalletPullToRefreshConfig {
return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = onRefreshSwipe)
}
}

View file

@ -8,16 +8,22 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.buttons.HorizontalActionChips
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.components.buttons.actions.RoundedActionButton
import com.tangem.core.ui.components.marketprice.MarketPriceBlock
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.transactions.Transaction
import com.tangem.core.ui.res.TangemTheme
@ -28,12 +34,11 @@ import com.tangem.feature.wallet.presentation.common.component.TokenItem
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.ui.components.WalletBottomSheet
import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletTopBar
import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TransactionsBlockGroupTitle
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TransactionsBlockTitle
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.WalletManageButtons
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.WalletMarketplaceBlock
import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletContentItemDecoration
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
@ -44,6 +49,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimat
*
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalMaterialApi::class)
@Suppress("LongMethod")
@Composable
internal fun WalletScreen(state: WalletStateHolder) {
@ -56,88 +62,108 @@ internal fun WalletScreen(state: WalletStateHolder) {
val walletsListState = rememberLazyListState()
val changeableItemModifier = Modifier.changeWalletAnimator(walletsListState)
val pullRefreshState = rememberPullRefreshState(
refreshing = state.pullToRefreshConfig.isRefreshing,
onRefresh = state.pullToRefreshConfig.onRefresh,
)
LazyColumn(
Box(
modifier = Modifier
.padding(paddingValues = scaffoldPaddings)
.fillMaxSize(),
contentPadding = PaddingValues(vertical = TangemTheme.dimens.spacing8),
horizontalAlignment = Alignment.CenterHorizontally,
.pullRefresh(pullRefreshState),
) {
item {
WalletsList(
config = state.walletsListConfig,
lazyListState = walletsListState,
)
}
if (state is WalletStateHolder.SingleCurrencyContent) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(vertical = TangemTheme.dimens.spacing8),
horizontalAlignment = Alignment.CenterHorizontally,
) {
item {
WalletManageButtons(
buttons = state.buttons,
modifier = changeableItemModifier.padding(top = TangemTheme.dimens.spacing14),
WalletsList(
config = state.walletsListConfig,
lazyListState = walletsListState,
)
}
}
items(
items = state.notifications,
itemContent = { item ->
Notification(
state = item.state,
modifier = changeableItemModifier
.padding(top = TangemTheme.dimens.spacing14)
.padding(horizontal = TangemTheme.dimens.spacing16),
)
},
)
if (state is WalletStateHolder.SingleCurrencyContent) {
item {
WalletMarketplaceBlock(
state = state.marketplaceBlockState,
modifier = changeableItemModifier
.padding(top = TangemTheme.dimens.spacing14)
.padding(horizontal = TangemTheme.dimens.spacing16),
)
}
}
itemsIndexed(
items = state.contentItems,
key = { index, item ->
when (item) {
is WalletContentItemState.MultiCurrencyItem.NetworkGroupTitle -> item.networkName
is WalletContentItemState.MultiCurrencyItem.Token -> index
is WalletContentItemState.SingleCurrencyItem.Title -> index
is WalletContentItemState.SingleCurrencyItem.GroupTitle -> item.title
is WalletContentItemState.SingleCurrencyItem.Transaction -> index
is WalletContentItemState.Loading -> index
if (state is WalletStateHolder.SingleCurrencyContent) {
item {
HorizontalActionChips(
buttons = state.buttons,
modifier = changeableItemModifier
.padding(top = TangemTheme.dimens.spacing14),
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16),
)
}
},
itemContent = { index, item ->
ContentItem(
item = item,
modifier = changeableItemModifier.walletContentItemDecoration(
currentIndex = index,
lastIndex = state.contentItems.lastIndex,
),
)
},
)
}
if (state is WalletStateHolder.MultiCurrencyContent) {
item {
OrganizeTokensButton(
onClick = state.onOrganizeTokensClick,
modifier = changeableItemModifier
.padding(top = TangemTheme.dimens.spacing14)
.padding(horizontal = TangemTheme.dimens.spacing16),
)
items(
items = state.notifications,
itemContent = { item ->
Notification(
state = item.state,
modifier = changeableItemModifier
.padding(top = TangemTheme.dimens.spacing14)
.padding(horizontal = TangemTheme.dimens.spacing16),
)
},
)
if (state is WalletStateHolder.SingleCurrencyContent) {
item {
MarketPriceBlock(
state = state.marketPriceBlockState,
modifier = changeableItemModifier
.padding(top = TangemTheme.dimens.spacing14)
.padding(horizontal = TangemTheme.dimens.spacing16),
)
}
}
itemsIndexed(
items = state.contentItems,
key = { index, item ->
when (item) {
is WalletContentItemState.MultiCurrencyItem.NetworkGroupTitle -> item.networkName
is WalletContentItemState.MultiCurrencyItem.Token -> index
is WalletContentItemState.SingleCurrencyItem.Title -> index
is WalletContentItemState.SingleCurrencyItem.GroupTitle -> item.title
is WalletContentItemState.SingleCurrencyItem.Transaction -> index
is WalletContentItemState.Loading -> index
}
},
itemContent = { index, item ->
ContentItem(
item = item,
modifier = changeableItemModifier.walletContentItemDecoration(
currentIndex = index,
lastIndex = state.contentItems.lastIndex,
),
)
},
)
if (state is WalletStateHolder.MultiCurrencyContent) {
item {
OrganizeTokensButton(
onClick = state.onOrganizeTokensClick,
modifier = changeableItemModifier
.padding(top = TangemTheme.dimens.spacing14)
.padding(horizontal = TangemTheme.dimens.spacing16),
)
}
}
}
PullRefreshIndicator(
refreshing = state.pullToRefreshConfig.isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
)
}
}
state.bottomSheet?.let { bottomSheetConfig ->
if (bottomSheetConfig.isShow) WalletBottomSheet(config = bottomSheetConfig)
}
}
@Composable

View file

@ -0,0 +1,161 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.PrimaryButtonIconStart
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.WalletBottomSheetConfig
/**
* Wallet bottom sheet with detail notification information
*
* @param config component config
*
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun WalletBottomSheet(config: WalletBottomSheetConfig) {
ModalBottomSheet(
onDismissRequest = config.onDismissRequest,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
containerColor = TangemTheme.colors.background.primary,
dragHandle = { BottomSheetDefaults.DragHandle() },
) {
BottomSheetContent(config = config.content)
}
}
@Composable
private fun BottomSheetContent(config: WalletBottomSheetConfig.BottomSheetContentConfig) {
Column(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(top = TangemTheme.dimens.spacing40, bottom = TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing40),
horizontalAlignment = Alignment.CenterHorizontally,
) {
val iconTint = config.tint
if (iconTint != null) {
Icon(
painter = painterResource(id = config.iconResId),
contentDescription = null,
modifier = Modifier.size(size = TangemTheme.dimens.size48),
tint = iconTint,
)
} else {
Image(
painter = painterResource(id = config.iconResId),
contentDescription = null,
modifier = Modifier.size(size = TangemTheme.dimens.size48),
)
}
Text(
text = config.title.resolveReference(),
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
style = TangemTheme.typography.h2,
)
Text(
text = config.subtitle.resolveReference(),
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
style = TangemTheme.typography.body2,
)
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)
}
}
}
}
@Composable
private fun PrimaryButton(
config: WalletBottomSheetConfig.BottomSheetContentConfig.ButtonConfig,
modifier: Modifier = Modifier,
) {
if (config.iconResId == null) {
PrimaryButton(
text = config.text,
onClick = config.onClick,
modifier = modifier,
)
} else {
PrimaryButtonIconStart(
text = config.text,
iconResId = config.iconResId,
onClick = config.onClick,
modifier = modifier,
)
}
}
@Composable
private fun SecondaryButton(
config: WalletBottomSheetConfig.BottomSheetContentConfig.ButtonConfig,
modifier: Modifier = Modifier,
) {
if (config.iconResId == null) {
SecondaryButton(
text = config.text,
onClick = config.onClick,
modifier = modifier,
)
} else {
SecondaryButtonIconStart(
text = config.text,
iconResId = config.iconResId,
onClick = config.onClick,
modifier = modifier,
)
}
}
@Preview
@Composable
private fun WalletBottomSheetContent_Light(
@PreviewParameter(WalletBottomSheetConfigProvider::class)
config: WalletBottomSheetConfig,
) {
TangemTheme(isDark = false) {
// Use preview of content because ModalBottomSheet isn't supported in Preview mode
BottomSheetContent(config = config.content)
}
}
@Preview
@Composable
private fun WalletBottomSheetContent_Dark(
@PreviewParameter(WalletBottomSheetConfigProvider::class)
config: WalletBottomSheetConfig,
) {
TangemTheme(isDark = false) {
// Use preview of content because ModalBottomSheet isn't supported in Preview mode
BottomSheetContent(config = config.content)
}
}
private class WalletBottomSheetConfigProvider : CollectionPreviewParameterProvider<WalletBottomSheetConfig>(
collection = listOf(WalletPreviewData.bottomSheet),
)

View file

@ -19,10 +19,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.WalletsListConfig
import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector
import kotlinx.coroutines.InternalCoroutinesApi
/**
* Wallets list component
@ -32,7 +30,7 @@ import kotlinx.coroutines.InternalCoroutinesApi
*
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalFoundationApi::class, InternalCoroutinesApi::class)
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState, modifier: Modifier = Modifier) {
val horizontalCardPadding = TangemTheme.dimens.spacing16
@ -45,7 +43,7 @@ internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState),
) {
items(items = config.wallets, key = WalletCardState::id) { state ->
items(items = config.wallets, key = { it.id.stringValue }) { state ->
WalletCard(state = state, modifier = Modifier.width(itemWidth))
}
}

View file

@ -1,67 +0,0 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.buttons.actions.ActionButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.WalletManageButton
import kotlinx.collections.immutable.ImmutableList
/**
* Wallet manage buttons
*
* @param buttons manage buttons
* @param modifier modifier
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun WalletManageButtons(buttons: ImmutableList<WalletManageButton>, modifier: Modifier = Modifier) {
LazyRow(
modifier = modifier
.background(color = TangemTheme.colors.background.secondary)
.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16),
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
verticalAlignment = Alignment.CenterVertically,
) {
items(
items = buttons,
key = { it.config.text },
itemContent = { ActionButton(config = it.config) },
)
}
}
@Preview
@Composable
private fun Preview_WalletManageButtons_Light(
@PreviewParameter(WalletManageButtonProvider::class) buttons: ImmutableList<WalletManageButton>,
) {
TangemTheme(isDark = false) {
WalletManageButtons(buttons = buttons)
}
}
@Preview
@Composable
private fun Preview_WalletManageButtons_Dark(
@PreviewParameter(WalletManageButtonProvider::class) buttons: ImmutableList<WalletManageButton>,
) {
TangemTheme(isDark = true) {
WalletManageButtons(buttons = buttons)
}
}
private class WalletManageButtonProvider : CollectionPreviewParameterProvider<ImmutableList<WalletManageButton>>(
collection = listOf(WalletPreviewData.manageButtons),
)

View file

@ -1,158 +0,0 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
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.Dp
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.common.state.PriceChangeConfig
import com.tangem.feature.wallet.presentation.wallet.state.WalletMarketplaceBlockState
/**
* Wallet marketplace block
*
* @param state state
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun WalletMarketplaceBlock(state: WalletMarketplaceBlockState, modifier: Modifier = Modifier) {
var rootWidth by remember { mutableStateOf(value = 0) }
Column(
modifier = modifier
.background(
color = TangemTheme.colors.background.primary,
shape = RoundedCornerShape(TangemTheme.dimens.radius14),
)
.heightIn(min = TangemTheme.dimens.size70)
.fillMaxWidth()
.padding(all = TangemTheme.dimens.spacing14)
.onSizeChanged { rootWidth = it.width },
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6),
horizontalAlignment = Alignment.Start,
) {
Text(
text = stringResource(id = R.string.wallet_marketplace_block_title, state.currencyName),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
)
when (state) {
is WalletMarketplaceBlockState.Loading -> {
RectangleShimmer(
modifier = Modifier.size(width = TangemTheme.dimens.size158, height = TangemTheme.dimens.size20),
)
}
is WalletMarketplaceBlockState.Content -> {
Price(
config = state,
priceWidthDp = with(LocalDensity.current) { rootWidth.div(other = 2).toDp() },
)
}
}
}
}
@Composable
private fun Price(config: WalletMarketplaceBlockState.Content, priceWidthDp: Dp) {
Row(
modifier = Modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
) {
Text(
text = config.price,
modifier = Modifier.widthIn(max = priceWidthDp),
color = TangemTheme.colors.text.primary1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography.body2,
)
PriceChangeInPercent(config.priceChangeConfig)
Text(
text = stringResource(id = R.string.wallet_marketprice_block_update_time),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
)
}
}
@Composable
private fun PriceChangeInPercent(config: PriceChangeConfig) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing4),
) {
Image(
painter = painterResource(
id = when (config.type) {
PriceChangeConfig.Type.UP -> R.drawable.img_arrow_up_8
PriceChangeConfig.Type.DOWN -> R.drawable.img_arrow_down_8
},
),
contentDescription = null,
)
Text(
text = config.valueInPercent,
color = when (config.type) {
PriceChangeConfig.Type.UP -> TangemTheme.colors.text.accent
PriceChangeConfig.Type.DOWN -> TangemTheme.colors.text.warning
},
style = TangemTheme.typography.body2,
)
}
}
@Preview
@Composable
private fun Preview_MarketplaceBlock_Light(
@PreviewParameter(WalletMarketplaceStateProvider::class)
state: WalletMarketplaceBlockState,
) {
TangemTheme(isDark = false) {
WalletMarketplaceBlock(state = state)
}
}
@Preview
@Composable
private fun Preview_MarketplaceBlock_Dark(
@PreviewParameter(WalletMarketplaceStateProvider::class)
state: WalletMarketplaceBlockState,
) {
TangemTheme(isDark = true) {
WalletMarketplaceBlock(state = state)
}
}
private class WalletMarketplaceStateProvider : CollectionPreviewParameterProvider<WalletMarketplaceBlockState>(
collection = listOf(
WalletPreviewData.marketplaceBlockContent,
WalletPreviewData.marketplaceBlockContent.copy(
priceChangeConfig = PriceChangeConfig(
valueInPercent = "5.16%",
type = PriceChangeConfig.Type.DOWN,
),
),
WalletMarketplaceBlockState.Loading(currencyName = "BTC"),
),
)

View file

@ -12,21 +12,27 @@ import com.tangem.core.ui.res.TangemTheme
*/
internal fun Modifier.walletContentItemDecoration(currentIndex: Int, lastIndex: Int): Modifier = composed {
val modifierWithHorizontalPadding = this.padding(horizontal = TangemTheme.dimens.spacing16)
when (currentIndex) {
0 -> {
val isSingleItem = currentIndex == 0 && lastIndex == 0
when {
isSingleItem -> {
modifierWithHorizontalPadding
.padding(top = TangemTheme.dimens.spacing14)
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
}
currentIndex == 0 -> {
modifierWithHorizontalPadding
.padding(top = TangemTheme.dimens.spacing14)
.clip(
RoundedCornerShape(
shape = RoundedCornerShape(
topStart = TangemTheme.dimens.radius16,
topEnd = TangemTheme.dimens.radius16,
),
)
}
lastIndex -> {
currentIndex == lastIndex -> {
modifierWithHorizontalPadding
.clip(
RoundedCornerShape(
shape = RoundedCornerShape(
bottomStart = TangemTheme.dimens.radius16,
bottomEnd = TangemTheme.dimens.radius16,
),

View file

@ -0,0 +1,40 @@
package com.tangem.feature.wallet.presentation.wallet.utils
import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount
import com.tangem.domain.tokens.model.TokenList
import com.tangem.feature.wallet.presentation.wallet.state.WalletCardState
import com.tangem.utils.converter.Converter
internal class FiatBalanceToWalletCardConverter(
private val currentState: WalletCardState,
private val isWalletContentHidden: Boolean,
private val fiatCurrencyCode: String,
private val fiatCurrencySymbol: String,
) : Converter<TokenList.FiatBalance, WalletCardState> {
override fun convert(value: TokenList.FiatBalance): WalletCardState {
// TODO: [REDACTED_JIRA]
return when (value) {
is TokenList.FiatBalance.Loading -> with(currentState) {
WalletCardState.Loading(id, title, additionalInfo, imageResId, onClick)
}
is TokenList.FiatBalance.Failed -> with(currentState) {
WalletCardState.Error(id, title, additionalInfo, imageResId, onClick)
}
is TokenList.FiatBalance.Loaded -> with(currentState) {
if (isWalletContentHidden) {
WalletCardState.HiddenContent(id, title, additionalInfo, imageResId, onClick)
} else {
WalletCardState.Content(
id = id,
title = title,
additionalInfo = additionalInfo,
imageResId = imageResId,
onClick = onClick,
balance = formatFiatAmount(value.amount, fiatCurrencyCode, fiatCurrencySymbol),
)
}
}
}
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.feature.wallet.presentation.wallet.utils
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState.MultiCurrencyItem
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
internal object LoadingItemsProvider {
fun getLoadingMultiCurrencyTokens(): PersistentList<MultiCurrencyItem> {
return List(size = 5) { TokenItemState.Loading }
.map { MultiCurrencyItem.Token(it) }
.toPersistentList()
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.feature.wallet.presentation.wallet.utils
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder
import com.tangem.utils.converter.Converter
internal class TokenListErrorToWalletStateConverter(
private val currentState: WalletStateHolder,
) : Converter<TokenListError, WalletStateHolder> {
// TODO: [REDACTED_JIRA]
override fun convert(value: TokenListError): WalletStateHolder {
return currentState
}
}

View file

@ -0,0 +1,63 @@
package com.tangem.feature.wallet.presentation.wallet.utils
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.wallet.state.WalletContentItemState.MultiCurrencyItem
import com.tangem.feature.wallet.presentation.wallet.utils.LoadingItemsProvider.getLoadingMultiCurrencyTokens
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.persistentListOf
internal class TokenListToContentItemsConverter(
isWalletContentHidden: Boolean,
fiatCurrencyCode: String,
fiatCurrencySymbol: String,
) : Converter<TokenList, ImmutableList<MultiCurrencyItem>> {
private val tokenStatusConverter = TokenStatusToTokenItemConverter(
isWalletContentHidden,
fiatCurrencyCode,
fiatCurrencySymbol,
)
override fun convert(value: TokenList): ImmutableList<MultiCurrencyItem> {
return when (value) {
is TokenList.GroupedByNetwork -> value.mapToMultiCurrencyItems()
is TokenList.Ungrouped -> value.mapToMultiCurrencyItems()
is TokenList.NotInitialized -> getLoadingMultiCurrencyTokens()
}
}
private fun TokenList.GroupedByNetwork.mapToMultiCurrencyItems(): PersistentList<MultiCurrencyItem> {
return groups.fold(initial = persistentListOf()) { acc, group ->
acc.mutate { it.addGroup(group) }
}
}
private fun TokenList.Ungrouped.mapToMultiCurrencyItems(): PersistentList<MultiCurrencyItem> {
return tokens.fold(initial = persistentListOf()) { acc, token ->
acc.mutate { it.addToken(token) }
}
}
private fun MutableList<MultiCurrencyItem>.addGroup(group: NetworkGroup): List<MultiCurrencyItem> {
this.add(MultiCurrencyItem.NetworkGroupTitle(group.network.name))
group.tokens.forEach { token ->
this.addToken(token)
}
return this
}
private fun MutableList<MultiCurrencyItem>.addToken(token: TokenStatus): List<MultiCurrencyItem> {
val tokenItemState = tokenStatusConverter.convert(token)
this.add(MultiCurrencyItem.Token(tokenItemState))
return this
}
}

View file

@ -0,0 +1,59 @@
package com.tangem.feature.wallet.presentation.wallet.utils
import com.tangem.domain.tokens.model.TokenList
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.utils.converter.Converter
import kotlinx.collections.immutable.toPersistentList
internal class TokenListToWalletStateConverter(
private val currentState: WalletStateHolder,
private val isWalletContentHidden: Boolean,
private val fiatCurrencyCode: String,
private val fiatCurrencySymbol: String,
) : Converter<TokenList, 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 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),
)
}
private fun WalletStateHolder.updateSelectedWallet(fiatBalance: TokenList.FiatBalance): WalletsListConfig {
val selectedWalletIndex = walletsListConfig.selectedWalletIndex
val selectedWalletCard = walletsListConfig.wallets[selectedWalletIndex]
val converter = FiatBalanceToWalletCardConverter(
selectedWalletCard,
isWalletContentHidden,
fiatCurrencyCode,
fiatCurrencySymbol,
)
return walletsListConfig.copy(
wallets = walletsListConfig.wallets
.toPersistentList()
.set(selectedWalletIndex, converter.convert(fiatBalance)),
)
}
}

View file

@ -0,0 +1,105 @@
package com.tangem.feature.wallet.presentation.wallet.utils
import androidx.annotation.DrawableRes
import com.tangem.core.ui.components.marketprice.PriceChangeConfig
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.tokens.model.TokenStatus
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
internal class TokenStatusToTokenItemConverter(
private val isWalletContentHidden: Boolean,
private val fiatCurrencyCode: String,
private val fiatCurrencySymbol: String,
) : Converter<TokenStatus, TokenItemState> {
private val TokenStatus.networkIconResId: Int?
@DrawableRes get() {
// TODO: [REDACTED_JIRA]
return if (isCoin) null else R.drawable.img_eth_22
}
private val TokenStatus.tokenIconResId: Int
@DrawableRes get() {
// TODO: [REDACTED_JIRA]
return R.drawable.img_eth_22
}
override fun convert(value: TokenStatus): TokenItemState {
return when (value.value) {
is TokenStatus.Loading -> TokenItemState.Loading
is TokenStatus.Loaded,
is TokenStatus.Custom,
-> value.mapToTokenItemState()
// TODO: Add other token item states, currently not designed
is TokenStatus.MissedDerivation,
is TokenStatus.NoAccount,
is TokenStatus.Unreachable,
-> value.mapToUnreachableTokenItemState()
}
}
private fun TokenStatus.mapToTokenItemState(): TokenItemState.Content {
return TokenItemState.Content(
id = this.id.value,
name = this.name,
tokenIconUrl = this.iconUrl,
tokenIconResId = this.tokenIconResId,
networkIconResId = this.networkIconResId,
amount = getFormattedAmount(),
hasPending = value.hasTransactionsInProgress,
tokenOptions = if (isWalletContentHidden) {
TokenItemState.TokenOptionsState.Hidden(getPriceChangeConfig())
} else {
TokenItemState.TokenOptionsState.Visible(
fiatAmount = getFormattedFiatAmount(),
priceChange = getPriceChangeConfig(),
)
},
)
}
private fun TokenStatus.getFormattedAmount(): String {
val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN
return BigDecimalFormatter.formatCryptoAmount(amount, symbol, decimals)
}
private fun TokenStatus.getFormattedFiatAmount(): String {
val fiatAmount = value.fiatAmount ?: return UNKNOWN_AMOUNT_SIGN
return BigDecimalFormatter.formatFiatAmount(fiatAmount, fiatCurrencyCode, fiatCurrencySymbol)
}
private fun TokenStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable(
id = this.id.value,
name = this.name,
tokenIconUrl = this.iconUrl,
tokenIconResId = this.tokenIconResId,
networkIconResId = this.networkIconResId,
)
private fun TokenStatus.getPriceChangeConfig(): PriceChangeConfig {
val priceChange = value.priceChange
?: return PriceChangeConfig(UNKNOWN_AMOUNT_SIGN, PriceChangeConfig.Type.DOWN)
return PriceChangeConfig(
valueInPercent = BigDecimalFormatter.formatPercent(priceChange, useAbsoluteValue = true),
type = priceChange.getPriceChangeType(),
)
}
private fun BigDecimal?.getPriceChangeType(): PriceChangeConfig.Type {
return when {
this == null -> PriceChangeConfig.Type.DOWN
this < BigDecimal.ZERO -> PriceChangeConfig.Type.DOWN
else -> PriceChangeConfig.Type.UP
}
}
private companion object {
const val UNKNOWN_AMOUNT_SIGN = ""
}
}

View file

@ -1,15 +1,35 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
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.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.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.WalletStateHolder
import com.tangem.feature.wallet.presentation.wallet.state.WalletTopBarConfig
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.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
import kotlin.properties.Delegates
@ -18,42 +38,147 @@ import kotlin.properties.Delegates
*
[REDACTED_AUTHOR]
*/
@Suppress("LongParameterList")
@HiltViewModel
internal class WalletViewModel @Inject constructor() : ViewModel() {
internal class WalletViewModel @Inject constructor(
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 scanCardProcessor: ScanCardProcessor,
private val dispatchers: CoroutineDispatcherProvider,
) : ViewModel(), DefaultLifecycleObserver {
/** Feature router */
var router: InnerWalletRouter by Delegates.notNull()
/** Screen state */
var uiState by mutableStateOf(getInitialState())
private set
// TODO: [REDACTED_TASK_KEY] Use production data instead of WalletPreviewData
private fun getInitialState(): WalletStateHolder = WalletPreviewData.multicurrencyWalletScreenState.copy(
onBackClick = { router.popBackStack() },
topBarConfig = WalletTopBarConfig(
onScanCardClick = { router.openOrganizeTokensScreen() },
onMoreClick = { router.openDetailsScreen() },
),
walletsListConfig = WalletPreviewData.multicurrencyWalletScreenState.walletsListConfig.copy(
onWalletChange = ::selectWallet,
),
private val stateFactory = WalletStateFactory(
routerProvider = Provider { router },
onScanCardClick = ::onScanCardClick,
onWalletChange = ::changeWallet,
onRefreshSwipe = ::refreshContent,
)
// TODO: [REDACTED_TASK_KEY] Use production data instead of WalletPreviewData
private fun selectWallet(index: Int) {
if (uiState.walletsListConfig.selectedWalletIndex == index) return
/** Screen state */
var uiState by mutableStateOf(stateFactory.getInitialState())
private set
Log.i("WalletViewModel", "selectWallet: $index")
override fun onCreate(owner: LifecycleOwner) {
getWalletsUseCase()
.distinctUntilChanged()
.flowWithLifecycle(owner.lifecycle)
.onEach { wallets ->
if (wallets.isEmpty()) return@onEach
uiState = if (index % 2 == 0) {
WalletPreviewData.multicurrencyWalletScreenState.copy(
walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index),
)
} else {
WalletPreviewData.singleWalletScreenState.copy(
walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index),
)
uiState = stateFactory.getContentState(wallets = wallets)
updateContentItems()
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun updateContentItems() {
getTokenListUseCase(
userWalletId = uiState.walletsListConfig.wallets.get(
index = uiState.walletsListConfig.selectedWalletIndex,
).id,
)
.distinctUntilChanged()
.mapLatest(::updateStateWithTokenListOrError)
.onEach {
uiState = it.copySealed(
pullToRefreshConfig = uiState.pullToRefreshConfig.copy(isRefreshing = getRefreshingStatus()),
)
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
}
private fun getRefreshingStatus(): Boolean {
return uiState.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
}
}
private fun onScanCardClick() {
val prevRequestPolicyStatus = getBiometricsStatusUseCase()
// Update access the code policy according access code saving status
setAccessCodeRequestPolicyUseCase(isBiometricsRequestPolicy = getAccessCodeSavingStatusUseCase())
viewModelScope.launch(dispatchers.io) {
scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true)
.doOnSuccess {
// If card's public key is null then user wallet will be null
val userWallet = UserWalletBuilder(scanResponse = it).build()
if (userWallet != null) {
saveWalletUseCase(userWallet)
.onLeft {
// Rollback policy if card saving was failed
setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus)
}
} else {
// Rollback policy if card saving was failed
setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus)
}
}
.doOnFailure {
// Rollback policy if card scanning was failed
setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus)
}
}
}
private fun changeWallet(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
}
updateContentItems()
}
private fun refreshContent() {
uiState = uiState.copySealed(pullToRefreshConfig = uiState.pullToRefreshConfig.copy(isRefreshing = true))
updateContentItems()
}
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)
}
}