Updated on 2026-08-14
This commit is contained in:
commit
ea661f55ab
250 changed files with 6169 additions and 949 deletions
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ private fun Content(screen: SeedPhraseScreen, uiState: OnboardingSeedPhraseState
|
|||
state = uiState.introState,
|
||||
)
|
||||
}
|
||||
|
||||
SeedPhraseScreen.AboutSeedPhrase -> {
|
||||
AboutSeedPhraseScreen(
|
||||
state = uiState.aboutState,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.feature.tokendetails.presentation.router
|
||||
|
||||
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
|
||||
|
||||
internal interface InnerTokenDetailsRouter : TokenDetailsRouter {
|
||||
|
||||
fun popBackStack()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state
|
||||
|
||||
data class TokenDetailsTopAppBarConfig(
|
||||
val onBackClick: () -> Unit,
|
||||
val onMoreClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
@ -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"),
|
||||
),
|
||||
)
|
||||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = "—"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue