Updated on 2026-08-14
This commit is contained in:
commit
f0d03e3648
286 changed files with 6353 additions and 5898 deletions
|
|
@ -1,13 +1,17 @@
|
|||
package com.tangem.feature.referral.converters
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import com.tangem.datasource.api.tangemTech.models.ReferralResponse
|
||||
import com.tangem.feature.referral.domain.models.DiscountType
|
||||
import com.tangem.feature.referral.domain.models.ReferralData
|
||||
import com.tangem.feature.referral.domain.models.ReferralInfo
|
||||
import com.tangem.feature.referral.domain.models.TokenData
|
||||
import com.tangem.feature.referral.domain.models.*
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.isToday
|
||||
import com.tangem.utils.extensions.isYesterday
|
||||
import com.tangem.utils.safeValueOf
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import org.joda.time.format.DateTimeFormatterBuilder
|
||||
import org.joda.time.format.ISODateTimeFormat
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
class ReferralConverter @Inject constructor() : Converter<ReferralResponse, ReferralData> {
|
||||
|
|
@ -16,6 +20,7 @@ class ReferralConverter @Inject constructor() : Converter<ReferralResponse, Refe
|
|||
val conditions = value.conditions
|
||||
val referral = value.referral
|
||||
val tokenConverter = TokenConverter()
|
||||
val expectedAwardsConverter = ExpectedAwardsConverter()
|
||||
return if (referral != null) {
|
||||
ReferralData.ParticipantData(
|
||||
award = conditions.awards.firstOrNull()?.amount ?: 0,
|
||||
|
|
@ -32,6 +37,9 @@ class ReferralConverter @Inject constructor() : Converter<ReferralResponse, Refe
|
|||
ISODateTimeFormat.dateTimeParser().parseDateTime(referral.termsAcceptedAt)
|
||||
},
|
||||
),
|
||||
expectedAwards = value.expectedAwards?.let {
|
||||
expectedAwardsConverter.convert(it)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
ReferralData.NonParticipantData(
|
||||
|
|
@ -45,6 +53,47 @@ class ReferralConverter @Inject constructor() : Converter<ReferralResponse, Refe
|
|||
}
|
||||
}
|
||||
|
||||
private class ExpectedAwardsConverter : Converter<ReferralResponse.ExpectedAwards, ExpectedAwards> {
|
||||
|
||||
/** Example, 2 Aug, 2023 */
|
||||
private val dateFormatter by lazy {
|
||||
DateTimeFormatterBuilder()
|
||||
.appendDayOfMonth(1)
|
||||
.appendLiteral(' ')
|
||||
.appendMonthOfYearShortText()
|
||||
.appendLiteral(", ")
|
||||
.appendYear(4, 4)
|
||||
.toFormatter()
|
||||
.withLocale(Locale.getDefault())
|
||||
}
|
||||
|
||||
override fun convert(value: ReferralResponse.ExpectedAwards): ExpectedAwards {
|
||||
return ExpectedAwards(
|
||||
numberOfWallets = value.numberOfWallets,
|
||||
expectedAwards = value.list.map {
|
||||
ExpectedAward(
|
||||
paymentDate = it.paymentDate.toDateTimeAtStartOfDay().millis.toDateFormat(),
|
||||
amount = "${it.amount} ${it.currency}",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun Long.toDateFormat(): String {
|
||||
val localDate = DateTime(this, DateTimeZone.getDefault())
|
||||
return if (localDate.isToday() || localDate.isYesterday()) {
|
||||
DateUtils.getRelativeTimeSpanString(
|
||||
this,
|
||||
DateTime.now().millis,
|
||||
DateUtils.DAY_IN_MILLIS,
|
||||
DateUtils.FORMAT_ABBREV_RELATIVE,
|
||||
).toString()
|
||||
} else {
|
||||
dateFormatter.print(localDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenConverter : Converter<ReferralResponse.Conditions.Award.Token, TokenData> {
|
||||
|
||||
override fun convert(value: ReferralResponse.Conditions.Award.Token): TokenData {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ internal class ReferralRepositoryImpl @Inject constructor(
|
|||
override val isDemoMode: Boolean
|
||||
get() = demoModeDatasource.isDemoModeActive
|
||||
|
||||
override suspend fun getReferralStatus(walletId: String): ReferralData {
|
||||
override suspend fun getReferralData(walletId: String): ReferralData {
|
||||
return withContext(coroutineDispatcher.io) {
|
||||
referralConverter.convert(
|
||||
referralApi.getReferralStatus(
|
||||
|
|
|
|||
|
|
@ -20,9 +20,11 @@ internal class ReferralInteractorImpl(
|
|||
get() = repository.isDemoMode
|
||||
|
||||
override suspend fun getReferralStatus(): ReferralData {
|
||||
val refStatus = repository.getReferralStatus(userWalletManager.getWalletId())
|
||||
saveRefTokens(refStatus.tokens)
|
||||
return refStatus
|
||||
val referralData = repository.getReferralData(userWalletManager.getWalletId())
|
||||
|
||||
saveReferralTokens(referralData.tokens)
|
||||
|
||||
return referralData
|
||||
}
|
||||
|
||||
override suspend fun startReferral(): ReferralData {
|
||||
|
|
@ -37,7 +39,7 @@ internal class ReferralInteractorImpl(
|
|||
address = publicAddress,
|
||||
)
|
||||
} else {
|
||||
error("tokens for ref is empty")
|
||||
error("Tokens for ref is empty")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -53,7 +55,7 @@ internal class ReferralInteractorImpl(
|
|||
return derivationPath
|
||||
}
|
||||
|
||||
private fun saveRefTokens(tokens: List<TokenData>) {
|
||||
private fun saveReferralTokens(tokens: List<TokenData>) {
|
||||
tokensForReferral.clear()
|
||||
tokensForReferral.addAll(tokens)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ interface ReferralRepository {
|
|||
val isDemoMode: Boolean
|
||||
|
||||
/** Returns data object of [ReferralData] depends on user program status */
|
||||
suspend fun getReferralStatus(walletId: String): ReferralData
|
||||
suspend fun getReferralData(walletId: String): ReferralData
|
||||
|
||||
/** Starts user referral program */
|
||||
suspend fun startReferral(walletId: String, networkId: String, tokenId: String, address: String): ReferralData
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ sealed interface ReferralData {
|
|||
override val tosLink: String,
|
||||
override val tokens: List<TokenData>,
|
||||
val referral: ReferralInfo,
|
||||
val expectedAwards: ExpectedAwards?,
|
||||
) : ReferralData
|
||||
|
||||
/** Data class that used if user is not participant of program */
|
||||
|
|
@ -47,6 +48,16 @@ data class ReferralInfo(
|
|||
val termsAcceptedAt: DateTime?,
|
||||
)
|
||||
|
||||
data class ExpectedAwards(
|
||||
val numberOfWallets: Int,
|
||||
val expectedAwards: List<ExpectedAward>,
|
||||
)
|
||||
|
||||
data class ExpectedAward(
|
||||
val paymentDate: String,
|
||||
val amount: String,
|
||||
)
|
||||
|
||||
enum class DiscountType {
|
||||
PERCENTAGE, VALUE
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.feature.referral.models
|
||||
|
||||
import com.tangem.feature.referral.domain.models.ExpectedAwards
|
||||
|
||||
internal data class ReferralStateHolder(
|
||||
val headerState: HeaderState,
|
||||
val referralInfoState: ReferralInfoState,
|
||||
|
|
@ -26,6 +28,7 @@ internal data class ReferralStateHolder(
|
|||
val code: String,
|
||||
val shareLink: String,
|
||||
override val url: String,
|
||||
val expectedAwards: ExpectedAwards?,
|
||||
) : ReferralInfoState, ReferralInfoContentState
|
||||
|
||||
data class NonParticipantContent(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.verticalScroll
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -43,13 +44,16 @@ internal fun AgreementBottomSheetContent(url: String) {
|
|||
@Composable
|
||||
private fun AgreementHtmlView(url: String) {
|
||||
val state = rememberWebViewState(url)
|
||||
val isInPreviewMode = LocalInspectionMode.current
|
||||
WebView(
|
||||
state = state,
|
||||
modifier = Modifier.background(TangemTheme.colors.background.secondary),
|
||||
onCreated = {
|
||||
it.settings.apply {
|
||||
javaScriptEnabled = false
|
||||
allowFileAccess = false
|
||||
if (!isInPreviewMode) {
|
||||
it.settings.apply {
|
||||
javaScriptEnabled = false
|
||||
allowFileAccess = false
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
package com.tangem.feature.referral.ui
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
internal fun AwardText(
|
||||
startText: String,
|
||||
startTextColor: Color,
|
||||
startTextStyle: TextStyle,
|
||||
endText: String,
|
||||
endTextColor: Color,
|
||||
endTextStyle: TextStyle,
|
||||
cornersToRound: CornersToRound,
|
||||
) {
|
||||
Surface(
|
||||
shape = cornersToRound.getShape(),
|
||||
color = TangemTheme.colors.background.primary,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(TangemTheme.dimens.size48)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = startText,
|
||||
color = startTextColor,
|
||||
maxLines = 1,
|
||||
style = startTextStyle,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = endText,
|
||||
color = endTextColor,
|
||||
maxLines = 1,
|
||||
style = endTextStyle,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_AwardItem_Light() {
|
||||
TangemTheme {
|
||||
AwardText(
|
||||
startText = "startText",
|
||||
startTextColor = TangemTheme.colors.text.tertiary,
|
||||
startTextStyle = TangemTheme.typography.subtitle2,
|
||||
endText = "endText",
|
||||
endTextColor = TangemTheme.colors.text.primary1,
|
||||
endTextStyle = TangemTheme.typography.subtitle2,
|
||||
cornersToRound = CornersToRound.TOP_2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.feature.referral.ui
|
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
internal enum class CornersToRound {
|
||||
|
||||
ALL_4,
|
||||
TOP_2,
|
||||
BOTTOM_2,
|
||||
ZERO,
|
||||
;
|
||||
|
||||
@Suppress("TopLevelComposableFunctions")
|
||||
@Composable
|
||||
fun getShape(): RoundedCornerShape {
|
||||
val radius = TangemTheme.dimens.radius12
|
||||
return when (this) {
|
||||
ALL_4 -> RoundedCornerShape(radius)
|
||||
TOP_2 -> RoundedCornerShape(topStart = radius, topEnd = radius)
|
||||
BOTTOM_2 -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius)
|
||||
ZERO -> RoundedCornerShape(0.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,15 +2,19 @@ package com.tangem.feature.referral.ui
|
|||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -19,14 +23,16 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
|||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import com.tangem.core.ui.components.PrimaryStartIconButton
|
||||
import com.tangem.core.ui.components.SmallInfoCard
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.referral.domain.models.ExpectedAward
|
||||
import com.tangem.feature.referral.domain.models.ExpectedAwards
|
||||
import com.tangem.feature.referral.presentation.R
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -36,6 +42,7 @@ internal fun ParticipateBottomBlock(
|
|||
purchasedWalletCount: Int,
|
||||
code: String,
|
||||
shareLink: String,
|
||||
expectedAwards: ExpectedAwards?,
|
||||
onAgreementClick: () -> Unit,
|
||||
onShowCopySnackbar: () -> Unit,
|
||||
onCopyClick: () -> Unit,
|
||||
|
|
@ -50,14 +57,6 @@ internal fun ParticipateBottomBlock(
|
|||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
SmallInfoCard(
|
||||
startText = stringResource(id = R.string.referral_friends_bought_title),
|
||||
endText = pluralStringResource(
|
||||
id = R.plurals.referral_wallets_purchased_count,
|
||||
count = purchasedWalletCount,
|
||||
purchasedWalletCount,
|
||||
),
|
||||
)
|
||||
PersonalCodeCard(code = code)
|
||||
AdditionalButtons(
|
||||
code = code,
|
||||
|
|
@ -66,10 +65,187 @@ internal fun ParticipateBottomBlock(
|
|||
onCopyClick = onCopyClick,
|
||||
onShareClick = onShareClick,
|
||||
)
|
||||
CounterAndAwards(purchasedWalletCount = purchasedWalletCount, expectedAwards = expectedAwards)
|
||||
AgreementText(firstPartResId = R.string.referral_tos_enroled_prefix, onClick = onAgreementClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CounterAndAwards(purchasedWalletCount: Int, expectedAwards: ExpectedAwards?) {
|
||||
Column {
|
||||
Counter(purchasedWalletCount, expectedAwards)
|
||||
|
||||
if (expectedAwards != null) {
|
||||
Awards(expectedAwards)
|
||||
} else if (purchasedWalletCount != 0) {
|
||||
EmptyUpcomingPayments()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Counter(purchasedWalletCount: Int, expectedAwards: ExpectedAwards?) {
|
||||
val isExpectedAwardsPresent = expectedAwards != null
|
||||
|
||||
AwardText(
|
||||
startText = stringResource(id = R.string.referral_friends_bought_title),
|
||||
startTextColor = TangemTheme.colors.text.tertiary,
|
||||
startTextStyle = TangemTheme.typography.subtitle2,
|
||||
endText = pluralStringResource(
|
||||
id = R.plurals.referral_wallets_purchased_count,
|
||||
count = purchasedWalletCount,
|
||||
purchasedWalletCount,
|
||||
),
|
||||
endTextColor = TangemTheme.colors.text.primary1,
|
||||
endTextStyle = TangemTheme.typography.body2,
|
||||
cornersToRound = if (isExpectedAwardsPresent || purchasedWalletCount != 0) {
|
||||
CornersToRound.TOP_2
|
||||
} else {
|
||||
CornersToRound.ALL_4
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun Awards(expectedAwards: ExpectedAwards) {
|
||||
val elementsCountToShowInLessMode = 3
|
||||
val isExpanded = remember { mutableStateOf(false) }
|
||||
|
||||
Divider(
|
||||
color = TangemTheme.colors.stroke.primary,
|
||||
thickness = TangemTheme.dimens.size0_5,
|
||||
)
|
||||
AwardText(
|
||||
startText = stringResource(id = R.string.referral_expected_awards),
|
||||
startTextColor = TangemTheme.colors.text.tertiary,
|
||||
startTextStyle = TangemTheme.typography.subtitle2,
|
||||
endText = pluralStringResource(
|
||||
id = R.plurals.referral_number_of_wallets,
|
||||
count = expectedAwards.numberOfWallets,
|
||||
expectedAwards.numberOfWallets,
|
||||
),
|
||||
endTextColor = TangemTheme.colors.text.tertiary,
|
||||
endTextStyle = TangemTheme.typography.body2,
|
||||
cornersToRound = CornersToRound.ZERO,
|
||||
)
|
||||
|
||||
val initialItems = expectedAwards.expectedAwards.take(elementsCountToShowInLessMode)
|
||||
val extraItems = expectedAwards.expectedAwards.drop(elementsCountToShowInLessMode)
|
||||
|
||||
initialItems.forEachIndexed { index, expectedAward ->
|
||||
AwardText(
|
||||
startText = expectedAward.paymentDate,
|
||||
startTextColor = TangemTheme.colors.text.primary1,
|
||||
startTextStyle = TangemTheme.typography.subtitle2,
|
||||
endText = expectedAward.amount,
|
||||
endTextColor = TangemTheme.colors.text.primary1,
|
||||
endTextStyle = TangemTheme.typography.subtitle2,
|
||||
cornersToRound = if (index == initialItems.size - 1 && extraItems.isEmpty()) {
|
||||
CornersToRound.BOTTOM_2
|
||||
} else {
|
||||
CornersToRound.ZERO
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded.value,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = shrinkVertically() + fadeOut(),
|
||||
) {
|
||||
ExtraItems(extraItems = extraItems)
|
||||
}
|
||||
|
||||
if (expectedAwards.expectedAwards.size > elementsCountToShowInLessMode) {
|
||||
LessMoreButton(isExpanded = isExpanded)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyUpcomingPayments() {
|
||||
Divider(
|
||||
color = TangemTheme.colors.stroke.primary,
|
||||
thickness = TangemTheme.dimens.size0_5,
|
||||
)
|
||||
AwardText(
|
||||
startText = stringResource(id = R.string.referral_expected_awards),
|
||||
startTextColor = TangemTheme.colors.text.tertiary,
|
||||
startTextStyle = TangemTheme.typography.subtitle2,
|
||||
endText = "",
|
||||
endTextColor = TangemTheme.colors.text.tertiary,
|
||||
endTextStyle = TangemTheme.typography.body2,
|
||||
cornersToRound = CornersToRound.BOTTOM_2,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LessMoreButton(isExpanded: MutableState<Boolean>) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(
|
||||
bottomStart = TangemTheme.dimens.radius12,
|
||||
bottomEnd = TangemTheme.dimens.radius12,
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.background(TangemTheme.colors.background.primary),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(TangemTheme.dimens.size48)
|
||||
.clickable { isExpanded.value = !isExpanded.value }
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = if (isExpanded.value) {
|
||||
stringResource(id = R.string.referral_less)
|
||||
} else {
|
||||
stringResource(id = R.string.referral_more)
|
||||
},
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
|
||||
val chevronIcon = if (isExpanded.value) {
|
||||
painterResource(id = com.tangem.core.ui.R.drawable.ic_chevron_up_24)
|
||||
} else {
|
||||
painterResource(id = com.tangem.core.ui.R.drawable.ic_chevron_24)
|
||||
}
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size20),
|
||||
painter = chevronIcon,
|
||||
tint = TangemTheme.colors.text.tertiary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtraItems(extraItems: List<ExpectedAward>) {
|
||||
Column {
|
||||
extraItems.forEach { expectedAward ->
|
||||
AwardText(
|
||||
startText = expectedAward.paymentDate,
|
||||
startTextColor = TangemTheme.colors.text.primary1,
|
||||
startTextStyle = TangemTheme.typography.subtitle2,
|
||||
endText = expectedAward.amount,
|
||||
endTextColor = TangemTheme.colors.text.primary1,
|
||||
endTextStyle = TangemTheme.typography.subtitle2,
|
||||
cornersToRound = CornersToRound.ZERO,
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PersonalCodeCard(code: String) {
|
||||
Column(
|
||||
|
|
@ -157,11 +333,28 @@ private fun Context.shareText(text: String) {
|
|||
@Composable
|
||||
private fun Preview_ParticipateBottomBlock_InLightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
Column(Modifier.background(TangemTheme.colors.background.primary)) {
|
||||
Column(Modifier.background(TangemTheme.colors.background.secondary)) {
|
||||
ParticipateBottomBlock(
|
||||
purchasedWalletCount = 3,
|
||||
code = "x4JdK",
|
||||
shareLink = "",
|
||||
expectedAwards = ExpectedAwards(
|
||||
numberOfWallets = 3,
|
||||
expectedAwards = listOf(
|
||||
ExpectedAward(
|
||||
amount = "10 USDT",
|
||||
paymentDate = "Today",
|
||||
),
|
||||
ExpectedAward(
|
||||
amount = "20 USDT",
|
||||
paymentDate = "6 Aug 2023",
|
||||
),
|
||||
ExpectedAward(
|
||||
amount = "30 USDT",
|
||||
paymentDate = "10 Aug 2023",
|
||||
),
|
||||
),
|
||||
),
|
||||
onAgreementClick = {},
|
||||
onShowCopySnackbar = {},
|
||||
onCopyClick = {},
|
||||
|
|
@ -173,13 +366,64 @@ private fun Preview_ParticipateBottomBlock_InLightTheme() {
|
|||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_ParticipateBottomBlock_InDarkTheme() {
|
||||
TangemTheme(isDark = true) {
|
||||
Column(Modifier.background(TangemTheme.colors.background.primary)) {
|
||||
private fun Preview_ParticipateBottomBlock_Without_Awards_InLightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
Column(Modifier.background(TangemTheme.colors.background.secondary)) {
|
||||
ParticipateBottomBlock(
|
||||
purchasedWalletCount = 3,
|
||||
code = "x4JdK",
|
||||
shareLink = "",
|
||||
expectedAwards = null,
|
||||
onAgreementClick = {},
|
||||
onShowCopySnackbar = {},
|
||||
onCopyClick = {},
|
||||
onShareClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_ParticipateBottomBlock_Without_Awards_And_Purchased_Wallets_InLightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
Column(Modifier.background(TangemTheme.colors.background.secondary)) {
|
||||
ParticipateBottomBlock(
|
||||
purchasedWalletCount = 0,
|
||||
code = "x4JdK",
|
||||
shareLink = "",
|
||||
expectedAwards = null,
|
||||
onAgreementClick = {},
|
||||
onShowCopySnackbar = {},
|
||||
onCopyClick = {},
|
||||
onShareClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Composable
|
||||
private fun LessMoreButton_White() {
|
||||
TangemTheme(isDark = false) {
|
||||
LessMoreButton(
|
||||
isExpanded = remember {
|
||||
mutableStateOf(false)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_ParticipateBottomBlock_Without_Awards_InDarkTheme() {
|
||||
TangemTheme(isDark = true) {
|
||||
Column(Modifier.background(TangemTheme.colors.background.secondary)) {
|
||||
ParticipateBottomBlock(
|
||||
purchasedWalletCount = 3,
|
||||
code = "x4JdK",
|
||||
shareLink = "",
|
||||
expectedAwards = null,
|
||||
onAgreementClick = {},
|
||||
onShowCopySnackbar = {},
|
||||
onCopyClick = {},
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import androidx.compose.ui.platform.LocalConfiguration
|
|||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
|
@ -31,6 +32,8 @@ import com.tangem.core.ui.components.SpacerH32
|
|||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.referral.domain.models.ExpectedAward
|
||||
import com.tangem.feature.referral.domain.models.ExpectedAwards
|
||||
import com.tangem.feature.referral.models.DemoModeException
|
||||
import com.tangem.feature.referral.models.ReferralStateHolder
|
||||
import com.tangem.feature.referral.models.ReferralStateHolder.*
|
||||
|
|
@ -159,6 +162,7 @@ private fun ReferralInfo(
|
|||
purchasedWalletCount = state.purchasedWalletCount,
|
||||
code = state.code,
|
||||
shareLink = state.shareLink,
|
||||
expectedAwards = state.expectedAwards,
|
||||
onAgreementClick = onAgreementClick,
|
||||
onShowCopySnackbar = onShowCopySnackbar,
|
||||
onCopyClick = stateHolder.analytics.onCopyClicked,
|
||||
|
|
@ -253,25 +257,47 @@ private fun Condition(@DrawableRes iconResId: Int, infoBlock: @Composable () ->
|
|||
private fun InfoForYou(award: String, networkName: String, address: String? = null) {
|
||||
ConditionInfo(title = stringResource(id = R.string.referral_point_currencies_title)) {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
append(stringResource(id = R.string.referral_point_currencies_description_prefix))
|
||||
withStyle(SpanStyle(color = TangemTheme.colors.text.primary1)) {
|
||||
append(" $award ")
|
||||
}
|
||||
append(
|
||||
String.format(
|
||||
stringResource(id = R.string.referral_point_currencies_description_suffix),
|
||||
networkName,
|
||||
if (!address.isNullOrBlank()) " $address" else "",
|
||||
),
|
||||
)
|
||||
},
|
||||
formatAwardConditionsString(
|
||||
quantity = award,
|
||||
network = networkName,
|
||||
address = if (!address.isNullOrBlank()) " $address" else "",
|
||||
),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun formatAwardConditionsString(quantity: String, network: String, address: String): AnnotatedString {
|
||||
val rawString = stringResource(R.string.referral_point_currencies_description, quantity, network, address)
|
||||
|
||||
val pattern = Regex("\\^\\^(.*?)\\^\\^")
|
||||
var startIndex = 0
|
||||
val annotatedString = buildAnnotatedString {
|
||||
pattern.findAll(rawString).forEach { matchResult ->
|
||||
val index = matchResult.range.first
|
||||
val matchedValue = matchResult.groups[1]?.value ?: ""
|
||||
|
||||
// appends unformatted part
|
||||
append(rawString.substring(startIndex, index))
|
||||
|
||||
// applies style on ^^-wrapped parts
|
||||
withStyle(SpanStyle(color = TangemTheme.colors.text.primary1)) {
|
||||
append(matchedValue)
|
||||
}
|
||||
|
||||
// goes to next part
|
||||
startIndex = matchResult.range.last + 1
|
||||
}
|
||||
|
||||
// appends remaining ending if exists
|
||||
append(rawString.substring(startIndex))
|
||||
}
|
||||
|
||||
return annotatedString
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoForYourFriend(discount: String) {
|
||||
ConditionInfo(title = stringResource(id = R.string.referral_point_discount_title)) {
|
||||
|
|
@ -464,6 +490,7 @@ private fun Preview_ReferralScreen_Participant_InLightTheme() {
|
|||
code = "x4JdK",
|
||||
shareLink = "",
|
||||
url = "",
|
||||
expectedAwards = null,
|
||||
),
|
||||
errorSnackbar = null,
|
||||
analytics = Analytics(
|
||||
|
|
@ -492,6 +519,52 @@ private fun Preview_ReferralScreen_Participant_InDarkTheme() {
|
|||
code = "x4JdK",
|
||||
shareLink = "",
|
||||
url = "",
|
||||
expectedAwards = null,
|
||||
),
|
||||
errorSnackbar = null,
|
||||
analytics = Analytics(
|
||||
onAgreementClicked = {},
|
||||
onCopyClicked = {},
|
||||
onShareClicked = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_ReferralScreen_Participant_With_Referrals_InLightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
ReferralScreen(
|
||||
stateHolder = ReferralStateHolder(
|
||||
headerState = HeaderState(onBackClicked = {}),
|
||||
referralInfoState = ReferralInfoState.ParticipantContent(
|
||||
award = "10 USDT",
|
||||
networkName = "Tron",
|
||||
address = "ma80...zk8q2",
|
||||
discount = "10%",
|
||||
purchasedWalletCount = 3,
|
||||
code = "x4JdK",
|
||||
shareLink = "",
|
||||
url = "",
|
||||
expectedAwards = ExpectedAwards(
|
||||
numberOfWallets = 5,
|
||||
expectedAwards = listOf(
|
||||
ExpectedAward(
|
||||
amount = "10 USDT",
|
||||
paymentDate = "Today",
|
||||
),
|
||||
ExpectedAward(
|
||||
amount = "20 USDT",
|
||||
paymentDate = "6 Aug 2023",
|
||||
),
|
||||
ExpectedAward(
|
||||
amount = "30 USDT",
|
||||
paymentDate = "10 Aug 2023",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
errorSnackbar = null,
|
||||
analytics = Analytics(
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ internal class ReferralViewModel @Inject constructor(
|
|||
|
||||
private var referralRouter: ReferralRouter by Delegates.notNull()
|
||||
|
||||
private val lastReferralData = mutableStateOf<ReferralData?>(null)
|
||||
private var lastReferralData: ReferralData? = null
|
||||
|
||||
init {
|
||||
loadReferralData()
|
||||
|
|
@ -67,7 +67,7 @@ internal class ReferralViewModel @Inject constructor(
|
|||
viewModelScope.launch(dispatchers.main) {
|
||||
runCatching(dispatchers.io) {
|
||||
referralInteractor.getReferralStatus().apply {
|
||||
lastReferralData.value = this
|
||||
lastReferralData = this
|
||||
}
|
||||
}
|
||||
.onSuccess(::showContent)
|
||||
|
|
@ -84,14 +84,13 @@ internal class ReferralViewModel @Inject constructor(
|
|||
viewModelScope.launch(dispatchers.main) {
|
||||
runCatching(dispatchers.io) { referralInteractor.startReferral() }
|
||||
.onSuccess(::showContent)
|
||||
.onFailure {
|
||||
if (it is UserCancelledException) {
|
||||
val lastRefData = lastReferralData.value
|
||||
if (lastRefData != null) {
|
||||
showContent(lastRefData)
|
||||
.onFailure { throwable ->
|
||||
if (throwable is UserCancelledException) {
|
||||
lastReferralData?.let { referralData ->
|
||||
showContent(referralData)
|
||||
}
|
||||
} else {
|
||||
showErrorSnackbar(it)
|
||||
showErrorSnackbar(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -130,6 +129,7 @@ internal class ReferralViewModel @Inject constructor(
|
|||
code = referral.promocode,
|
||||
shareLink = referral.shareLink,
|
||||
url = tosLink,
|
||||
expectedAwards = expectedAwards,
|
||||
)
|
||||
is ReferralData.NonParticipantData -> ReferralInfoState.NonParticipantContent(
|
||||
award = getAwardValue(),
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ dependencies {
|
|||
implementation(deps.compose.paging)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.ui.utils)
|
||||
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.jodatime)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ package com.tangem.feature.tokendetails.presentation.tokendetails
|
|||
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig
|
||||
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
|
||||
|
|
@ -13,7 +16,18 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
|
||||
internal object TokenDetailsPreviewData {
|
||||
|
||||
val tokenDetailsTopAppBarConfig = TokenDetailsTopAppBarConfig(onBackClick = {}, onMoreClick = {})
|
||||
val tokenDetailsTopAppBarConfig = TokenDetailsTopAppBarConfig(
|
||||
onBackClick = {},
|
||||
tokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig(
|
||||
persistentListOf(
|
||||
TokenDetailsAppBarMenuConfig.MenuItem(
|
||||
title = TextReference.Res(id = R.string.token_details_hide_token),
|
||||
textColorProvider = { TangemTheme.colors.text.warning },
|
||||
onClick = { },
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val tokenInfoBlockStateWithLongNameInMainCurrency = TokenInfoBlockState(
|
||||
name = "Stellar (XLM) with long name test",
|
||||
|
|
@ -67,5 +81,7 @@ internal object TokenDetailsPreviewData {
|
|||
value = TxHistoryState.getDefaultLoadingTransactions {},
|
||||
),
|
||||
),
|
||||
dialogConfig = null,
|
||||
pendingTxs = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class TokenDetailsAppBarMenuConfig(val items: ImmutableList<MenuItem>) {
|
||||
data class MenuItem(
|
||||
val title: TextReference,
|
||||
val textColorProvider: @Composable () -> Color,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,12 +1,17 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state
|
||||
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
||||
internal data class TokenDetailsState(
|
||||
val topAppBarConfig: TokenDetailsTopAppBarConfig,
|
||||
val tokenInfoBlockState: TokenInfoBlockState,
|
||||
val tokenBalanceBlockState: TokenDetailsBalanceBlockState,
|
||||
val marketPriceBlockState: MarketPriceBlockState,
|
||||
val pendingTxs: PersistentList<TransactionState>,
|
||||
val txHistoryState: TxHistoryState,
|
||||
val dialogConfig: TokenDetailsDialogConfig?,
|
||||
)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state
|
||||
|
||||
data class TokenDetailsTopAppBarConfig(
|
||||
internal data class TokenDetailsTopAppBarConfig(
|
||||
val onBackClick: () -> Unit,
|
||||
val onMoreClick: () -> Unit,
|
||||
val tokenDetailsAppBarMenuConfig: TokenDetailsAppBarMenuConfig,
|
||||
)
|
||||
|
|
@ -2,7 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
|
||||
data class TokenInfoBlockState(
|
||||
internal data class TokenInfoBlockState(
|
||||
val name: String,
|
||||
val iconUrl: String,
|
||||
val currency: Currency,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state.components
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.features.tokendetails.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
|
||||
*/
|
||||
internal data class TokenDetailsDialogConfig(
|
||||
val isShow: Boolean,
|
||||
val onDismissRequest: () -> Unit,
|
||||
val content: DialogContentConfig,
|
||||
) {
|
||||
|
||||
sealed class DialogContentConfig {
|
||||
|
||||
abstract val title: TextReference
|
||||
abstract val message: TextReference
|
||||
abstract val confirmButtonConfig: ButtonConfig
|
||||
abstract val cancelButtonConfig: ButtonConfig?
|
||||
|
||||
data class ButtonConfig(
|
||||
val text: TextReference,
|
||||
val onClick: () -> Unit,
|
||||
val warning: Boolean = false,
|
||||
)
|
||||
|
||||
data class ConfirmHideConfig(
|
||||
val currencySymbol: String,
|
||||
val onConfirmClick: () -> Unit,
|
||||
val onCancelClick: () -> Unit,
|
||||
) : DialogContentConfig() {
|
||||
override val title: TextReference = TextReference.Res(
|
||||
id = R.string.token_details_hide_alert_title,
|
||||
formatArgs = wrappedList(currencySymbol),
|
||||
)
|
||||
|
||||
override val message: TextReference = TextReference.Res(R.string.token_details_hide_alert_message)
|
||||
|
||||
override val cancelButtonConfig: ButtonConfig = ButtonConfig(
|
||||
text = TextReference.Res(R.string.common_cancel),
|
||||
onClick = onCancelClick,
|
||||
)
|
||||
|
||||
override val confirmButtonConfig: ButtonConfig = ButtonConfig(
|
||||
text = TextReference.Res(R.string.token_details_hide_alert_hide),
|
||||
onClick = onConfirmClick,
|
||||
warning = true,
|
||||
)
|
||||
}
|
||||
|
||||
data class HasLinkedTokensConfig(
|
||||
val currencySymbol: String,
|
||||
val networkName: String,
|
||||
val onConfirmClick: () -> Unit,
|
||||
) : DialogContentConfig() {
|
||||
override val title: TextReference = TextReference.Res(
|
||||
id = R.string.token_details_unable_hide_alert_title,
|
||||
formatArgs = wrappedList(currencySymbol),
|
||||
)
|
||||
|
||||
override val message: TextReference = TextReference.Res(
|
||||
id = R.string.token_details_unable_hide_alert_message,
|
||||
formatArgs = wrappedList(currencySymbol, networkName),
|
||||
)
|
||||
|
||||
override val cancelButtonConfig: ButtonConfig?
|
||||
get() = null
|
||||
|
||||
override val confirmButtonConfig: ButtonConfig = ButtonConfig(
|
||||
text = TextReference.Res(R.string.common_ok),
|
||||
onClick = onConfirmClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,14 +10,22 @@ import com.tangem.domain.tokens.error.CurrencyStatusError
|
|||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
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.factory.txhistory.TokenDetailsTxHistoryToTransactionStateConverter
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class TokenDetailsLoadedBalanceConverter(
|
||||
private val currentStateProvider: Provider<TokenDetailsState>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val symbol: String,
|
||||
private val decimals: Int,
|
||||
) : Converter<Either<CurrencyStatusError, CryptoCurrencyStatus>, TokenDetailsState> {
|
||||
|
||||
private val txHistoryItemConverter by lazy {
|
||||
TokenDetailsTxHistoryToTransactionStateConverter(symbol, decimals)
|
||||
}
|
||||
|
||||
override fun convert(value: Either<CurrencyStatusError, CryptoCurrencyStatus>): TokenDetailsState {
|
||||
return value.fold(ifLeft = { convertError() }, ifRight = ::convert)
|
||||
}
|
||||
|
|
@ -33,6 +41,7 @@ internal class TokenDetailsLoadedBalanceConverter(
|
|||
return state.copy(
|
||||
tokenBalanceBlockState = getBalanceState(state.tokenBalanceBlockState, status),
|
||||
marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName),
|
||||
pendingTxs = status.value.pendingTransactions.map(txHistoryItemConverter::convert).toPersistentList(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
|
|||
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.extensions.iconResId
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.networkIconResId
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
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.feature.tokendetails.presentation.tokendetails.state.*
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSkeletonStateConverter.SkeletonModel
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -24,7 +24,7 @@ internal class TokenDetailsSkeletonStateConverter(
|
|||
return TokenDetailsState(
|
||||
topAppBarConfig = TokenDetailsTopAppBarConfig(
|
||||
onBackClick = clickIntents::onBackClick,
|
||||
onMoreClick = clickIntents::onMoreClick,
|
||||
tokenDetailsAppBarMenuConfig = createMenu(),
|
||||
),
|
||||
tokenInfoBlockState = TokenInfoBlockState(
|
||||
name = value.cryptoCurrency.name,
|
||||
|
|
@ -34,7 +34,7 @@ internal class TokenDetailsSkeletonStateConverter(
|
|||
is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token(
|
||||
networkName = currency.network.standardType.name,
|
||||
blockchainName = currency.network.name,
|
||||
networkIcon = currency.iconResId,
|
||||
networkIcon = currency.networkIconResId,
|
||||
)
|
||||
},
|
||||
),
|
||||
|
|
@ -42,14 +42,26 @@ internal class TokenDetailsSkeletonStateConverter(
|
|||
actionButtons = createButtons(),
|
||||
),
|
||||
marketPriceBlockState = MarketPriceBlockState.Loading(value.cryptoCurrency.name),
|
||||
pendingTxs = persistentListOf(),
|
||||
txHistoryState = TxHistoryState.Content(
|
||||
contentItems = MutableStateFlow(
|
||||
value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick),
|
||||
),
|
||||
),
|
||||
dialogConfig = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMenu(): TokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig(
|
||||
items = persistentListOf(
|
||||
TokenDetailsAppBarMenuConfig.MenuItem(
|
||||
title = TextReference.Res(id = R.string.token_details_hide_token),
|
||||
textColorProvider = { TangemTheme.colors.text.warning },
|
||||
onClick = clickIntents::onHideClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private fun createButtons(): ImmutableList<TokenDetailsActionButton> {
|
||||
return persistentListOf(
|
||||
TokenDetailsActionButton.Buy(enabled = false, onClick = {}),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.tangem.domain.txhistory.models.TxHistoryItem
|
|||
import com.tangem.domain.txhistory.models.TxHistoryListError
|
||||
import com.tangem.domain.txhistory.models.TxHistoryStateError
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
||||
|
|
@ -33,6 +34,8 @@ internal class TokenDetailsStateFactory(
|
|||
TokenDetailsLoadedBalanceConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
symbol = symbol,
|
||||
decimals = decimals,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -81,4 +84,37 @@ internal class TokenDetailsStateFactory(
|
|||
): TokenDetailsState {
|
||||
return loadedTxHistoryConverter.convert(txHistoryEither)
|
||||
}
|
||||
|
||||
fun getStateWithClosedDialog(): TokenDetailsState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(dialogConfig = state.dialogConfig?.copy(isShow = false))
|
||||
}
|
||||
|
||||
fun getStateWithConfirmHideTokenDialog(currency: CryptoCurrency): TokenDetailsState {
|
||||
return currentStateProvider().copy(
|
||||
dialogConfig = TokenDetailsDialogConfig(
|
||||
isShow = true,
|
||||
onDismissRequest = clickIntents::onDismissDialog,
|
||||
content = TokenDetailsDialogConfig.DialogContentConfig.ConfirmHideConfig(
|
||||
currencySymbol = currency.symbol,
|
||||
onConfirmClick = clickIntents::onHideConfirmed,
|
||||
onCancelClick = clickIntents::onDismissDialog,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getStateWithLinkedTokensDialog(currency: CryptoCurrency): TokenDetailsState {
|
||||
return currentStateProvider().copy(
|
||||
dialogConfig = TokenDetailsDialogConfig(
|
||||
isShow = true,
|
||||
onDismissRequest = clickIntents::onDismissDialog,
|
||||
content = TokenDetailsDialogConfig.DialogContentConfig.HasLinkedTokensConfig(
|
||||
currencySymbol = currency.symbol,
|
||||
networkName = currency.network.name,
|
||||
onConfirmClick = clickIntents::onDismissDialog,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory
|
||||
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.toBriefAddressFormat
|
||||
import com.tangem.utils.toFormattedCurrencyString
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import org.joda.time.format.DateTimeFormatterBuilder
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
|
||||
internal class TokenDetailsTxHistoryToTransactionStateConverter(
|
||||
private val symbol: String,
|
||||
private val decimals: Int,
|
||||
) : Converter<TxHistoryItem, TransactionState> {
|
||||
|
||||
/** Example, 13:35 */
|
||||
private val timeFormatter by lazy {
|
||||
DateTimeFormatterBuilder()
|
||||
.appendHourOfDay(1)
|
||||
.appendLiteral(':')
|
||||
.appendMinuteOfHour(2)
|
||||
.toFormatter()
|
||||
.withLocale(Locale.getDefault())
|
||||
}
|
||||
|
||||
override fun convert(value: TxHistoryItem): TransactionState {
|
||||
return when (value.type) {
|
||||
TxHistoryItem.TransactionType.Transfer -> {
|
||||
when (val direction = value.direction) {
|
||||
is TxHistoryItem.TransactionDirection.Incoming -> {
|
||||
createIncomingTransferTransaction(value, direction)
|
||||
}
|
||||
is TxHistoryItem.TransactionDirection.Outgoing -> {
|
||||
createOutgoingTransferTransaction(value, direction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createIncomingTransferTransaction(
|
||||
item: TxHistoryItem,
|
||||
direction: TxHistoryItem.TransactionDirection.Incoming,
|
||||
): TransactionState {
|
||||
return when (item.status) {
|
||||
TxHistoryItem.TxStatus.Confirmed -> TransactionState.Receive(
|
||||
txHash = item.txHash,
|
||||
address = direction.extractAddress(),
|
||||
amount = item.amount.toCryptoCurrencyFormat(),
|
||||
timestamp = timeFormatter.print(DateTime(item.timestampInMillis, DateTimeZone.getDefault())),
|
||||
)
|
||||
TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Receiving(
|
||||
txHash = item.txHash,
|
||||
address = direction.extractAddress(),
|
||||
amount = item.amount.toCryptoCurrencyFormat(),
|
||||
timestamp = timeFormatter.print(DateTime(item.timestampInMillis, DateTimeZone.getDefault())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createOutgoingTransferTransaction(
|
||||
item: TxHistoryItem,
|
||||
direction: TxHistoryItem.TransactionDirection.Outgoing,
|
||||
): TransactionState {
|
||||
return when (item.status) {
|
||||
TxHistoryItem.TxStatus.Confirmed -> TransactionState.Send(
|
||||
txHash = item.txHash,
|
||||
address = direction.extractAddress(),
|
||||
amount = item.amount.toCryptoCurrencyFormat(),
|
||||
timestamp = timeFormatter.print(DateTime(item.timestampInMillis, DateTimeZone.getDefault())),
|
||||
)
|
||||
TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Sending(
|
||||
txHash = item.txHash,
|
||||
address = direction.extractAddress(),
|
||||
amount = item.amount.toCryptoCurrencyFormat(),
|
||||
timestamp = timeFormatter.print(DateTime(item.timestampInMillis, DateTimeZone.getDefault())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal.toCryptoCurrencyFormat(): String {
|
||||
return toFormattedCurrencyString(currency = symbol, decimals = decimals)
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.TransactionDirection.extractAddress(): TextReference = when (val addr = address) {
|
||||
TxHistoryItem.Address.Multiple -> TextReference.Res(R.string.transaction_history_multiple_addresses)
|
||||
is TxHistoryItem.Address.Single -> TextReference.Str(addr.rawAddress.toBriefAddressFormat())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,33 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
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.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlock
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.transactions.Transaction
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.components.transactions.txHistoryItems
|
||||
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.TokenDetailsDialogs
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
||||
@Composable
|
||||
internal fun TokenDetailsScreen(state: TokenDetailsState) {
|
||||
|
|
@ -54,8 +64,31 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
|
|||
contentType = MarketPriceBlockState::class.java,
|
||||
content = { MarketPriceBlock(modifier = itemModifier, state = state.marketPriceBlockState) },
|
||||
)
|
||||
if (state.txHistoryState is TxHistoryState.NotSupported && state.pendingTxs.isNotEmpty()) {
|
||||
item {
|
||||
PendingTxsBlock(
|
||||
pendingTxs = state.pendingTxs,
|
||||
modifier = itemModifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
txHistoryItems(state = state.txHistoryState, txHistoryItems = txHistoryItems)
|
||||
}
|
||||
|
||||
TokenDetailsDialogs(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PendingTxsBlock(pendingTxs: PersistentList<TransactionState>, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(color = TangemTheme.colors.background.primary),
|
||||
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
pendingTxs.fastForEach { Transaction(state = it) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,229 @@
|
|||
@file:Suppress("TopLevelPropertyNaming")
|
||||
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupPositionProvider
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
|
||||
/**
|
||||
* Just copy paste [DropdownMenu] from material3 with deleting vertical paddings.
|
||||
*/
|
||||
@Composable
|
||||
internal fun TangemDropdownMenu(
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
offset: DpOffset = DpOffset(0.dp, 0.dp),
|
||||
properties: PopupProperties = PopupProperties(focusable = true),
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val expandedStates = remember { MutableTransitionState(false) }
|
||||
expandedStates.targetState = expanded
|
||||
|
||||
if (expandedStates.currentState || expandedStates.targetState) {
|
||||
val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) }
|
||||
val density = LocalDensity.current
|
||||
val popupPositionProvider = DropdownMenuPositionProvider(
|
||||
offset,
|
||||
density,
|
||||
) { parentBounds, menuBounds ->
|
||||
transformOriginState.value = calculateTransformOrigin(parentBounds, menuBounds)
|
||||
}
|
||||
|
||||
Popup(
|
||||
onDismissRequest = onDismissRequest,
|
||||
popupPositionProvider = popupPositionProvider,
|
||||
properties = properties,
|
||||
) {
|
||||
DropdownMenuContent(
|
||||
expandedStates = expandedStates,
|
||||
transformOriginState = transformOriginState,
|
||||
modifier = modifier,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val InTransitionDuration = 120
|
||||
private const val OutTransitionDuration = 75
|
||||
|
||||
@Suppress("ReusedModifierInstance", "MagicNumber")
|
||||
@Composable
|
||||
private fun DropdownMenuContent(
|
||||
expandedStates: MutableTransitionState<Boolean>,
|
||||
transformOriginState: MutableState<TransformOrigin>,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
// Menu open/close animation.
|
||||
val transition = updateTransition(expandedStates, "DropDownMenu")
|
||||
|
||||
val scale by transition.animateFloat(
|
||||
transitionSpec = {
|
||||
if (false isTransitioningTo true) {
|
||||
// Dismissed to expanded
|
||||
tween(
|
||||
durationMillis = InTransitionDuration,
|
||||
easing = LinearOutSlowInEasing,
|
||||
)
|
||||
} else {
|
||||
// Expanded to dismissed.
|
||||
tween(
|
||||
durationMillis = 1,
|
||||
delayMillis = OutTransitionDuration - 1,
|
||||
)
|
||||
}
|
||||
},
|
||||
label = "",
|
||||
) {
|
||||
if (it) {
|
||||
// Menu is expanded.
|
||||
1f
|
||||
} else {
|
||||
// Menu is dismissed.
|
||||
0.8f
|
||||
}
|
||||
}
|
||||
|
||||
val alpha by transition.animateFloat(
|
||||
transitionSpec = {
|
||||
if (false isTransitioningTo true) {
|
||||
// Dismissed to expanded
|
||||
tween(durationMillis = 30)
|
||||
} else {
|
||||
// Expanded to dismissed.
|
||||
tween(durationMillis = OutTransitionDuration)
|
||||
}
|
||||
},
|
||||
label = "",
|
||||
) {
|
||||
if (it) {
|
||||
// Menu is expanded.
|
||||
1f
|
||||
} else {
|
||||
// Menu is dismissed.
|
||||
0f
|
||||
}
|
||||
}
|
||||
Card(
|
||||
modifier = Modifier.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
this.alpha = alpha
|
||||
transformOrigin = transformOriginState.value
|
||||
},
|
||||
elevation = CardDefaults.cardElevation(),
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(IntrinsicSize.Max)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect): TransformOrigin {
|
||||
val pivotX = when {
|
||||
menuBounds.left >= parentBounds.right -> 0f
|
||||
menuBounds.right <= parentBounds.left -> 1f
|
||||
menuBounds.width == 0 -> 0f
|
||||
else -> {
|
||||
val intersectionCenter =
|
||||
(
|
||||
kotlin.math.max(parentBounds.left, menuBounds.left) +
|
||||
kotlin.math.min(parentBounds.right, menuBounds.right)
|
||||
) / 2
|
||||
(intersectionCenter - menuBounds.left).toFloat() / menuBounds.width
|
||||
}
|
||||
}
|
||||
val pivotY = when {
|
||||
menuBounds.top >= parentBounds.bottom -> 0f
|
||||
menuBounds.bottom <= parentBounds.top -> 1f
|
||||
menuBounds.height == 0 -> 0f
|
||||
else -> {
|
||||
val intersectionCenter =
|
||||
(
|
||||
kotlin.math.max(parentBounds.top, menuBounds.top) +
|
||||
kotlin.math.min(parentBounds.bottom, menuBounds.bottom)
|
||||
) / 2
|
||||
(intersectionCenter - menuBounds.top).toFloat() / menuBounds.height
|
||||
}
|
||||
}
|
||||
return TransformOrigin(pivotX, pivotY)
|
||||
}
|
||||
|
||||
private val MenuVerticalMargin = 48.dp
|
||||
|
||||
@Immutable
|
||||
internal data class DropdownMenuPositionProvider(
|
||||
val contentOffset: DpOffset,
|
||||
val density: Density,
|
||||
val onPositionCalculated: (IntRect, IntRect) -> Unit = { _, _ -> },
|
||||
) : PopupPositionProvider {
|
||||
override fun calculatePosition(
|
||||
anchorBounds: IntRect,
|
||||
windowSize: IntSize,
|
||||
layoutDirection: LayoutDirection,
|
||||
popupContentSize: IntSize,
|
||||
): IntOffset {
|
||||
// The min margin above and below the menu, relative to the screen.
|
||||
val verticalMargin = with(density) { MenuVerticalMargin.roundToPx() }
|
||||
// The content offset specified using the dropdown offset parameter.
|
||||
val contentOffsetX = with(density) { contentOffset.x.roundToPx() }
|
||||
val contentOffsetY = with(density) { contentOffset.y.roundToPx() }
|
||||
|
||||
// Compute horizontal position.
|
||||
val toRight = anchorBounds.left + contentOffsetX
|
||||
val toLeft = anchorBounds.right - contentOffsetX - popupContentSize.width
|
||||
val toDisplayRight = windowSize.width - popupContentSize.width
|
||||
val toDisplayLeft = 0
|
||||
val x = if (layoutDirection == LayoutDirection.Ltr) {
|
||||
sequenceOf(
|
||||
toRight,
|
||||
toLeft,
|
||||
// If the anchor gets outside of the window on the left, we want to position
|
||||
// toDisplayLeft for proximity to the anchor. Otherwise, toDisplayRight.
|
||||
if (anchorBounds.left >= 0) toDisplayRight else toDisplayLeft,
|
||||
)
|
||||
} else {
|
||||
sequenceOf(
|
||||
toLeft,
|
||||
toRight,
|
||||
// If the anchor gets outside of the window on the right, we want to position
|
||||
// toDisplayRight for proximity to the anchor. Otherwise, toDisplayLeft.
|
||||
if (anchorBounds.right <= windowSize.width) toDisplayLeft else toDisplayRight,
|
||||
)
|
||||
}.firstOrNull {
|
||||
it >= 0 && it + popupContentSize.width <= windowSize.width
|
||||
} ?: toLeft
|
||||
|
||||
// Compute vertical position.
|
||||
val toBottom = maxOf(anchorBounds.bottom + contentOffsetY, verticalMargin)
|
||||
val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height
|
||||
val toCenter = anchorBounds.top - popupContentSize.height / 2
|
||||
val toDisplayBottom = windowSize.height - popupContentSize.height - verticalMargin
|
||||
val y = sequenceOf(toBottom, toTop, toCenter, toDisplayBottom).firstOrNull {
|
||||
it >= verticalMargin &&
|
||||
it + popupContentSize.height <= windowSize.height - verticalMargin
|
||||
} ?: toTop
|
||||
|
||||
onPositionCalculated(
|
||||
anchorBounds,
|
||||
IntRect(x, y, x + popupContentSize.width, y + popupContentSize.height),
|
||||
)
|
||||
return IntOffset(x, y)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.tangem.core.ui.components.BasicDialog
|
||||
import com.tangem.core.ui.components.DialogButton
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
|
||||
|
||||
@Composable
|
||||
internal fun TokenDetailsDialogs(state: TokenDetailsState) {
|
||||
val dialogConfig = state.dialogConfig
|
||||
if (dialogConfig != null && dialogConfig.isShow) {
|
||||
TokenDetailsDialog(config = dialogConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenDetailsDialog(config: TokenDetailsDialogConfig) {
|
||||
BasicDialog(
|
||||
message = config.content.message.resolveReference(),
|
||||
confirmButton = DialogButton(
|
||||
title = config.content.confirmButtonConfig.text.resolveReference(),
|
||||
warning = config.content.confirmButtonConfig.warning,
|
||||
onClick = config.content.confirmButtonConfig.onClick,
|
||||
),
|
||||
onDismissDialog = config.onDismissRequest,
|
||||
title = config.content.title.resolveReference(),
|
||||
dismissButton = config.content.cancelButtonConfig?.let { cancelButtonConfig ->
|
||||
DialogButton(
|
||||
title = cancelButtonConfig.text.resolveReference(),
|
||||
warning = cancelButtonConfig.warning,
|
||||
onClick = cancelButtonConfig.onClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,17 +1,31 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
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.TokenDetailsAppBarMenuConfig
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) {
|
||||
var showDropdownMenu by rememberSaveable { mutableStateOf(false) }
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = config.onBackClick) {
|
||||
|
|
@ -24,13 +38,28 @@ internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) {
|
|||
},
|
||||
title = {},
|
||||
actions = {
|
||||
IconButton(onClick = config.onMoreClick) {
|
||||
IconButton(onClick = { showDropdownMenu = true }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_more_vertical_24),
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
contentDescription = "More",
|
||||
)
|
||||
}
|
||||
|
||||
TangemDropdownMenu(
|
||||
expanded = showDropdownMenu,
|
||||
modifier = Modifier.background(TangemTheme.colors.background.primary),
|
||||
onDismissRequest = { showDropdownMenu = false },
|
||||
offset = DpOffset(x = TangemTheme.dimens.spacing20, y = TangemTheme.dimens.spacing10.times(-1)),
|
||||
content = {
|
||||
config.tokenDetailsAppBarMenuConfig.items.fastForEach {
|
||||
AppBarDropdownItem(
|
||||
item = it,
|
||||
dismissParent = { showDropdownMenu = false },
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
|
|
@ -41,6 +70,57 @@ internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) {
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("ComposableEventParameterNaming")
|
||||
@Composable
|
||||
private fun AppBarDropdownItem(
|
||||
item: TokenDetailsAppBarMenuConfig.MenuItem,
|
||||
dismissParent: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier
|
||||
.clickable {
|
||||
dismissParent()
|
||||
item.onClick()
|
||||
}
|
||||
.padding(vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing16),
|
||||
text = item.title.resolveReference(),
|
||||
style = TangemTheme.typography.body1.copy(color = item.textColorProvider()),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_TokenDetailsAppBarDropdownItem_LightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
AppBarDropdownItem(
|
||||
modifier = Modifier.background(TangemTheme.colors.background.primary),
|
||||
dismissParent = {},
|
||||
item = TokenDetailsAppBarMenuConfig.MenuItem(
|
||||
title = TextReference.Res(id = R.string.token_details_hide_token),
|
||||
textColorProvider = { TangemTheme.colors.text.warning },
|
||||
onClick = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_TokenDetailsAppBarDropdownItem_DarkTheme() {
|
||||
TangemTheme(isDark = true) {
|
||||
AppBarDropdownItem(
|
||||
modifier = Modifier.background(TangemTheme.colors.background.primary),
|
||||
dismissParent = {},
|
||||
item = TokenDetailsAppBarMenuConfig.MenuItem(
|
||||
title = TextReference.Res(id = R.string.token_details_hide_token),
|
||||
textColorProvider = { TangemTheme.colors.text.warning },
|
||||
onClick = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_TokenDetailsTopAppBar_LightTheme() {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ interface TokenDetailsClickIntents : TxHistoryClickIntents {
|
|||
|
||||
fun onBackClick()
|
||||
|
||||
fun onMoreClick()
|
||||
|
||||
fun onSendClick()
|
||||
|
||||
fun onReceiveClick()
|
||||
|
|
@ -15,4 +13,10 @@ interface TokenDetailsClickIntents : TxHistoryClickIntents {
|
|||
fun onSellClick()
|
||||
|
||||
fun onSwapClick()
|
||||
|
||||
fun onDismissDialog()
|
||||
|
||||
fun onHideClick()
|
||||
|
||||
fun onHideConfirmed()
|
||||
}
|
||||
|
|
@ -12,6 +12,8 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
|
||||
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase
|
||||
import com.tangem.domain.tokens.RemoveCurrencyUseCase
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
|
|
@ -31,6 +33,7 @@ import com.tangem.utils.coroutines.saveIn
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
|
|
@ -45,6 +48,8 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
|
||||
private val getExploreUrlUseCase: GetExploreUrlUseCase,
|
||||
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
|
||||
private val removeCurrencyUseCase: RemoveCurrencyUseCase,
|
||||
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents {
|
||||
|
|
@ -84,12 +89,12 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
|
||||
private fun updateContent(selectedWallet: UserWallet) {
|
||||
updateMarketPrice(selectedWallet = selectedWallet)
|
||||
updateButtons(userWalletId = selectedWallet.walletId, currencyId = cryptoCurrency.id.value)
|
||||
updateButtons(userWalletId = selectedWallet.walletId, currency = cryptoCurrency)
|
||||
updateTxHistory()
|
||||
}
|
||||
|
||||
private fun updateButtons(userWalletId: UserWalletId, currencyId: String) {
|
||||
getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, tokenId = currencyId)
|
||||
private fun updateButtons(userWalletId: UserWalletId, currency: CryptoCurrency) {
|
||||
getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, cryptoCurrency = currency)
|
||||
.distinctUntilChanged()
|
||||
.onEach { uiState = stateFactory.getManageButtonsState(actions = it.states) }
|
||||
.flowOn(dispatchers.io)
|
||||
|
|
@ -149,10 +154,6 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
router.popBackStack()
|
||||
}
|
||||
|
||||
override fun onMoreClick() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun onBuyClick() {
|
||||
val status = cryptoCurrencyStatus ?: return
|
||||
|
||||
|
|
@ -170,7 +171,40 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onSendClick() {
|
||||
reduxStateHolder.dispatch(TradeCryptoAction.New.Send)
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatus ?: return
|
||||
|
||||
when (cryptoCurrencyStatus.currency) {
|
||||
is CryptoCurrency.Coin -> {
|
||||
reduxStateHolder.dispatch(
|
||||
action = TradeCryptoAction.New.SendCoin(
|
||||
userWallet = wallet,
|
||||
coinStatus = cryptoCurrencyStatus,
|
||||
),
|
||||
)
|
||||
}
|
||||
is CryptoCurrency.Token -> sendToken(status = cryptoCurrencyStatus)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendToken(status: CryptoCurrencyStatus) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
getNetworkCoinStatusUseCase(
|
||||
userWalletId = wallet.walletId,
|
||||
networkId = status.currency.network.id,
|
||||
)
|
||||
.take(count = 1)
|
||||
.collectLatest {
|
||||
it.onRight { coinStatus ->
|
||||
reduxStateHolder.dispatch(
|
||||
action = TradeCryptoAction.New.SendToken(
|
||||
userWallet = wallet,
|
||||
tokenStatus = status,
|
||||
coinFiatRate = coinStatus.value.fiatRate,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceiveClick() {
|
||||
|
|
@ -191,6 +225,29 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
reduxStateHolder.dispatch(TradeCryptoAction.New.Swap(cryptoCurrency))
|
||||
}
|
||||
|
||||
override fun onDismissDialog() {
|
||||
uiState = stateFactory.getStateWithClosedDialog()
|
||||
}
|
||||
|
||||
override fun onHideClick() {
|
||||
viewModelScope.launch {
|
||||
val hasLinkedTokens = removeCurrencyUseCase.hasLinkedTokens(wallet.walletId, cryptoCurrency)
|
||||
uiState = if (hasLinkedTokens) {
|
||||
stateFactory.getStateWithLinkedTokensDialog(cryptoCurrency)
|
||||
} else {
|
||||
stateFactory.getStateWithConfirmHideTokenDialog(cryptoCurrency)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onHideConfirmed() {
|
||||
viewModelScope.launch {
|
||||
removeCurrencyUseCase.invoke(wallet.walletId, cryptoCurrency)
|
||||
.onLeft { Timber.e(it) }
|
||||
.onRight { router.popBackStack() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onExploreClick() {
|
||||
viewModelScope.launch {
|
||||
router.openUrl(
|
||||
|
|
|
|||
|
|
@ -8,13 +8,18 @@ import com.tangem.core.ui.components.transactions.state.TransactionState
|
|||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.event.consumed
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
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.model.DraggableItem
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.*
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.*
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -87,12 +92,34 @@ internal object WalletPreviewData {
|
|||
)
|
||||
}
|
||||
|
||||
private val coinIconState
|
||||
get() = TokenItemState.IconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = R.drawable.img_polygon_22,
|
||||
isGrayscale = false,
|
||||
)
|
||||
|
||||
private val tokenIconState
|
||||
get() = TokenItemState.IconState.TokenIcon(
|
||||
url = null,
|
||||
networkBadgeIconResId = R.drawable.img_polygon_22,
|
||||
fallbackTint = TangemColorPalette.Black,
|
||||
fallbackBackground = TangemColorPalette.Meadow,
|
||||
isGrayscale = false,
|
||||
)
|
||||
|
||||
private val customTokenIconState
|
||||
get() = TokenItemState.IconState.CustomTokenIcon(
|
||||
tint = TangemColorPalette.Black,
|
||||
background = TangemColorPalette.Meadow,
|
||||
networkBadgeIconResId = R.drawable.img_polygon_22,
|
||||
isGrayscale = false,
|
||||
)
|
||||
|
||||
val tokenItemVisibleState by lazy {
|
||||
TokenItemState.Content(
|
||||
id = UUID.randomUUID().toString(),
|
||||
tokenIconUrl = null,
|
||||
tokenIconResId = R.drawable.img_polygon_22,
|
||||
networkBadgeIconResId = R.drawable.img_polygon_22,
|
||||
icon = coinIconState,
|
||||
name = "Polygon",
|
||||
amount = "5,412 MATIC",
|
||||
hasPending = true,
|
||||
|
|
@ -103,7 +130,6 @@ internal object WalletPreviewData {
|
|||
type = PriceChangeConfig.Type.UP,
|
||||
),
|
||||
),
|
||||
isTestnet = false,
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
)
|
||||
|
|
@ -112,16 +138,14 @@ internal object WalletPreviewData {
|
|||
val testnetTokenItemVisibleState by lazy {
|
||||
tokenItemVisibleState.copy(
|
||||
name = "Polygon testnet",
|
||||
isTestnet = true,
|
||||
icon = tokenIconState.copy(isGrayscale = true),
|
||||
)
|
||||
}
|
||||
|
||||
val tokenItemHiddenState by lazy {
|
||||
TokenItemState.Content(
|
||||
id = UUID.randomUUID().toString(),
|
||||
tokenIconUrl = null,
|
||||
tokenIconResId = R.drawable.img_polygon_22,
|
||||
networkBadgeIconResId = R.drawable.img_polygon_22,
|
||||
icon = tokenIconState,
|
||||
name = "Polygon",
|
||||
amount = "5,412 MATIC",
|
||||
hasPending = true,
|
||||
|
|
@ -131,7 +155,6 @@ internal object WalletPreviewData {
|
|||
type = PriceChangeConfig.Type.UP,
|
||||
),
|
||||
),
|
||||
isTestnet = false,
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
)
|
||||
|
|
@ -140,25 +163,37 @@ internal object WalletPreviewData {
|
|||
val tokenItemDragState by lazy {
|
||||
TokenItemState.Draggable(
|
||||
id = UUID.randomUUID().toString(),
|
||||
tokenIconUrl = null,
|
||||
tokenIconResId = R.drawable.img_polygon_22,
|
||||
networkBadgeIconResId = R.drawable.img_polygon_22,
|
||||
icon = tokenIconState,
|
||||
name = "Polygon",
|
||||
isTestnet = false,
|
||||
fiatAmount = "3 172,14 $",
|
||||
info = stringReference(value = "3 172,14 $"),
|
||||
)
|
||||
}
|
||||
|
||||
val tokenItemUnreachableState by lazy {
|
||||
TokenItemState.Unreachable(
|
||||
id = UUID.randomUUID().toString(),
|
||||
tokenIconUrl = null,
|
||||
tokenIconResId = R.drawable.img_polygon_22,
|
||||
networkBadgeIconResId = R.drawable.img_polygon_22,
|
||||
icon = tokenIconState,
|
||||
name = "Polygon",
|
||||
)
|
||||
}
|
||||
|
||||
val customTokenItemVisibleState by lazy {
|
||||
tokenItemVisibleState.copy(
|
||||
name = "Polygon custom",
|
||||
icon = customTokenIconState.copy(
|
||||
tint = TangemColorPalette.White,
|
||||
background = TangemColorPalette.Black,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val customTestnetTokenItemVisibleState by lazy {
|
||||
tokenItemVisibleState.copy(
|
||||
name = "Polygon custom testnet",
|
||||
icon = customTokenIconState.copy(isGrayscale = true),
|
||||
)
|
||||
}
|
||||
|
||||
val loadingTokenItemState by lazy { TokenItemState.Loading(id = "Loading#1") }
|
||||
|
||||
private const val networksSize = 10
|
||||
|
|
@ -188,7 +223,6 @@ internal object WalletPreviewData {
|
|||
tokenItemState = tokenItemDragState.copy(
|
||||
id = "${group.id}_token_$tokenNumber",
|
||||
name = "Token $tokenNumber from $networkNumber network",
|
||||
networkBadgeIconResId = R.drawable.img_eth_22.takeIf { i != 0 },
|
||||
),
|
||||
groupId = group.id,
|
||||
roundingMode = when {
|
||||
|
|
@ -199,7 +233,7 @@ internal object WalletPreviewData {
|
|||
)
|
||||
}
|
||||
|
||||
val divider = DraggableItem.GroupPlaceholder(id = "divider_$networkNumber")
|
||||
val divider = DraggableItem.Placeholder(id = "divider_$networkNumber")
|
||||
|
||||
buildList {
|
||||
add(group)
|
||||
|
|
@ -234,7 +268,7 @@ internal object WalletPreviewData {
|
|||
),
|
||||
dndConfig = OrganizeTokensState.DragAndDropConfig(
|
||||
onItemDragged = { _, _ -> },
|
||||
onDragStart = {},
|
||||
onItemDragStart = {},
|
||||
canDragItemOver = { _, _ -> false },
|
||||
onItemDragEnd = {},
|
||||
),
|
||||
|
|
@ -299,8 +333,6 @@ internal object WalletPreviewData {
|
|||
tokenItemVisibleState.copy(
|
||||
id = "token_1",
|
||||
name = "Ethereum",
|
||||
tokenIconResId = R.drawable.img_eth_22,
|
||||
networkBadgeIconResId = null,
|
||||
amount = "1,89340821 ETH",
|
||||
),
|
||||
),
|
||||
|
|
@ -308,8 +340,6 @@ internal object WalletPreviewData {
|
|||
tokenItemVisibleState.copy(
|
||||
id = "token_2",
|
||||
name = "Ethereum",
|
||||
tokenIconResId = R.drawable.img_eth_22,
|
||||
networkBadgeIconResId = null,
|
||||
amount = "1,89340821 ETH",
|
||||
),
|
||||
),
|
||||
|
|
@ -317,8 +347,6 @@ internal object WalletPreviewData {
|
|||
tokenItemVisibleState.copy(
|
||||
id = "token_3",
|
||||
name = "Ethereum",
|
||||
tokenIconResId = R.drawable.img_eth_22,
|
||||
networkBadgeIconResId = null,
|
||||
amount = "1,89340821 ETH",
|
||||
),
|
||||
),
|
||||
|
|
@ -326,8 +354,6 @@ internal object WalletPreviewData {
|
|||
tokenItemVisibleState.copy(
|
||||
id = "token_4",
|
||||
name = "Ethereum",
|
||||
tokenIconResId = R.drawable.img_eth_22,
|
||||
networkBadgeIconResId = null,
|
||||
amount = "1,89340821 ETH",
|
||||
),
|
||||
),
|
||||
|
|
@ -336,13 +362,11 @@ internal object WalletPreviewData {
|
|||
tokenItemVisibleState.copy(
|
||||
id = "token_5",
|
||||
name = "Ethereum",
|
||||
tokenIconResId = R.drawable.img_eth_22,
|
||||
networkBadgeIconResId = null,
|
||||
amount = "1,89340821 ETH",
|
||||
),
|
||||
),
|
||||
),
|
||||
onOrganizeTokensClick = {},
|
||||
organizeTokensButton = WalletTokensListState.OrganizeTokensButtonState.Visible(isEnabled = true, {}),
|
||||
),
|
||||
pullToRefreshConfig = WalletPullToRefreshConfig(
|
||||
isRefreshing = false,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,15 @@ package com.tangem.feature.wallet.presentation.common.component
|
|||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -17,37 +21,21 @@ import androidx.constraintlayout.compose.ConstrainedLayoutReference
|
|||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.constraintlayout.compose.ConstraintLayoutScope
|
||||
import androidx.constraintlayout.compose.Dimension
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
|
||||
import com.tangem.feature.wallet.presentation.common.component.token.TokenCryptoInfoBlock
|
||||
import com.tangem.feature.wallet.presentation.common.component.token.TokenFiatInfoBlock
|
||||
import com.tangem.feature.wallet.presentation.common.component.token.TokenIcon
|
||||
import com.tangem.feature.wallet.presentation.common.component.token.icon.TokenIcon
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import org.burnoutcrew.reorderable.ReorderableLazyListState
|
||||
|
||||
// TODO: Add custom token state: [REDACTED_JIRA]
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
internal fun TokenItem(
|
||||
state: TokenItemState,
|
||||
modifier: Modifier = Modifier,
|
||||
reorderableTokenListState: ReorderableLazyListState? = null,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val containerModifier: Modifier = remember(state) {
|
||||
when (state) {
|
||||
is TokenItemState.Content -> modifier.combinedClickable(
|
||||
onClick = state.onItemClick,
|
||||
onLongClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
state.onItemLongClick()
|
||||
},
|
||||
)
|
||||
else -> modifier
|
||||
}
|
||||
}
|
||||
BaseContainer(modifier = containerModifier) {
|
||||
BaseContainer(modifier = modifier.tokenClickable(state)) {
|
||||
val (iconRef, cryptoInfoRef, fiatInfoRef) = createRefs()
|
||||
|
||||
TokenIcon(
|
||||
|
|
@ -90,7 +78,7 @@ private inline fun BaseContainer(
|
|||
) {
|
||||
ConstraintLayout(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing14,
|
||||
vertical = TangemTheme.dimens.spacing14,
|
||||
|
|
@ -110,8 +98,32 @@ private fun Modifier.constrainAsOptionsItem(scope: ConstraintLayoutScope, ref: C
|
|||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun Modifier.tokenClickable(state: TokenItemState): Modifier = composed {
|
||||
when (state) {
|
||||
is TokenItemState.Content -> {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val onLongClick = remember(state) {
|
||||
{
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
state.onItemLongClick()
|
||||
}
|
||||
}
|
||||
|
||||
this.combinedClickable(
|
||||
onClick = state.onItemClick,
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
}
|
||||
is TokenItemState.Draggable,
|
||||
is TokenItemState.Unreachable,
|
||||
is TokenItemState.Loading,
|
||||
is TokenItemState.Locked,
|
||||
-> this
|
||||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_Tokens_LightTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) {
|
||||
|
|
@ -136,6 +148,8 @@ private class TokenConfigProvider : CollectionPreviewParameterProvider<TokenItem
|
|||
WalletPreviewData.tokenItemHiddenState,
|
||||
WalletPreviewData.loadingTokenItemState,
|
||||
WalletPreviewData.testnetTokenItemVisibleState,
|
||||
WalletPreviewData.customTokenItemVisibleState,
|
||||
WalletPreviewData.customTestnetTokenItemVisibleState,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemTypography
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
|
|
@ -40,7 +41,7 @@ private fun ContentBlock(state: TokenItemState.ContentState, modifier: Modifier
|
|||
AmountText(
|
||||
amount = when (state) {
|
||||
is TokenItemState.Content -> if (state.tokenOptions is TokenOptionsState.Hidden) DOTS else state.amount
|
||||
is TokenItemState.Draggable -> state.fiatAmount
|
||||
is TokenItemState.Draggable -> state.info.resolveReference()
|
||||
is TokenItemState.Unreachable -> null
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,152 +0,0 @@
|
|||
package com.tangem.feature.wallet.presentation.common.component.token
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.ColorMatrix
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
|
||||
private const val GRAY_SCALE_SATURATION = 0f
|
||||
|
||||
@Composable
|
||||
internal fun TokenIcon(state: TokenItemState, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is TokenItemState.ContentState -> ContentIcon(content = state, modifier = modifier)
|
||||
is TokenItemState.Loading -> LoadingIcon(modifier = modifier)
|
||||
is TokenItemState.Locked -> LockedIcon(modifier = modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContentIcon(content: TokenItemState.ContentState, modifier: Modifier = Modifier) {
|
||||
BaseContainer(modifier = modifier) {
|
||||
val isTestnet = when (content) {
|
||||
is TokenItemState.Content -> content.isTestnet
|
||||
is TokenItemState.Draggable -> content.isTestnet
|
||||
is TokenItemState.Unreachable -> false
|
||||
}
|
||||
|
||||
val colorFilter = remember(isTestnet) {
|
||||
if (isTestnet) {
|
||||
ColorFilter.colorMatrix(
|
||||
colorMatrix = ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) },
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
content = content,
|
||||
colorFilter = colorFilter,
|
||||
modifier = Modifier.align(Alignment.BottomStart),
|
||||
)
|
||||
|
||||
NetworkBadge(
|
||||
iconResId = content.networkBadgeIconResId,
|
||||
colorFilter = colorFilter,
|
||||
modifier = Modifier.align(Alignment.TopEnd),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Icon(content: TokenItemState.ContentState, colorFilter: ColorFilter?, modifier: Modifier = Modifier) {
|
||||
val iconUrl = content.tokenIconUrl
|
||||
val iconData: Any = remember(iconUrl) {
|
||||
if (iconUrl.isNullOrEmpty()) content.tokenIconResId else iconUrl
|
||||
}
|
||||
|
||||
SubcomposeAsyncImage(
|
||||
modifier = modifier.iconSize(),
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(data = iconData)
|
||||
.placeholder(drawableResId = content.tokenIconResId)
|
||||
.error(drawableResId = content.tokenIconResId)
|
||||
.fallback(drawableResId = content.tokenIconResId)
|
||||
.crossfade(enable = true)
|
||||
.build(),
|
||||
colorFilter = colorFilter,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.NetworkBadge(
|
||||
@DrawableRes iconResId: Int?,
|
||||
colorFilter: ColorFilter?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = iconResId != null,
|
||||
modifier = modifier
|
||||
.size(TangemTheme.dimens.size18)
|
||||
.background(color = TangemTheme.colors.background.primary, shape = CircleShape),
|
||||
) {
|
||||
if (iconResId == null) return@AnimatedVisibility
|
||||
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.padding(all = TangemTheme.dimens.spacing2)
|
||||
.align(Alignment.Center),
|
||||
painter = painterResource(id = iconResId),
|
||||
colorFilter = colorFilter,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingIcon(modifier: Modifier = Modifier) {
|
||||
BaseContainer(modifier) {
|
||||
CircleShimmer(
|
||||
modifier = Modifier
|
||||
.iconSize()
|
||||
.align(alignment = Alignment.BottomStart),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LockedIcon(modifier: Modifier = Modifier) {
|
||||
BaseContainer(modifier) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.iconSize()
|
||||
.align(Alignment.BottomStart),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(color = TangemTheme.colors.background.secondary, shape = CircleShape),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private inline fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) {
|
||||
Box(modifier = modifier.size(size = TangemTheme.dimens.size40), content = content)
|
||||
}
|
||||
|
||||
private fun Modifier.iconSize(): Modifier = composed {
|
||||
return@composed this.size(size = TangemTheme.dimens.size36)
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
package com.tangem.feature.wallet.presentation.common.component.token.icon
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
|
||||
@Composable
|
||||
internal fun ContentIcon(icon: TokenItemState.IconState, colorFilter: ColorFilter?, modifier: Modifier = Modifier) {
|
||||
when (icon) {
|
||||
is TokenItemState.IconState.CoinIcon -> CoinIcon(
|
||||
modifier = modifier,
|
||||
url = icon.url,
|
||||
fallbackResId = icon.fallbackResId,
|
||||
colorFilter = colorFilter,
|
||||
)
|
||||
is TokenItemState.IconState.TokenIcon -> TokenIcon(
|
||||
modifier = modifier,
|
||||
url = icon.url,
|
||||
colorFilter = colorFilter,
|
||||
errorIcon = {
|
||||
CustomTokenIcon(
|
||||
modifier = modifier,
|
||||
tint = icon.fallbackTint,
|
||||
background = icon.fallbackBackground,
|
||||
)
|
||||
},
|
||||
)
|
||||
is TokenItemState.IconState.CustomTokenIcon -> CustomTokenIcon(
|
||||
modifier = modifier,
|
||||
tint = icon.tint,
|
||||
background = icon.background,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CoinIcon(
|
||||
url: String?,
|
||||
@DrawableRes fallbackResId: Int,
|
||||
colorFilter: ColorFilter?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url
|
||||
|
||||
DefaultCurrencyIcon(
|
||||
modifier = modifier,
|
||||
iconData = iconData,
|
||||
errorIcon = {
|
||||
Image(
|
||||
painter = painterResource(id = fallbackResId),
|
||||
colorFilter = colorFilter,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
colorFilter = colorFilter,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenIcon(
|
||||
url: String?,
|
||||
colorFilter: ColorFilter?,
|
||||
errorIcon: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (url == null) {
|
||||
errorIcon()
|
||||
} else {
|
||||
DefaultCurrencyIcon(
|
||||
modifier = modifier,
|
||||
iconData = url,
|
||||
errorIcon = errorIcon,
|
||||
colorFilter = colorFilter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CustomTokenIcon(tint: Color, background: Color, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = background,
|
||||
shape = CircleShape,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.matchParentSize(),
|
||||
painter = painterResource(id = R.drawable.ic_custom_token_44),
|
||||
tint = tint,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private inline fun DefaultCurrencyIcon(
|
||||
iconData: Any,
|
||||
colorFilter: ColorFilter?,
|
||||
crossinline errorIcon: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = modifier,
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(iconData)
|
||||
.crossfade(enable = true)
|
||||
.build(),
|
||||
loading = { LoadingIcon() },
|
||||
error = { errorIcon() },
|
||||
colorFilter = colorFilter,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.tangem.feature.wallet.presentation.common.component.token.icon
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
internal fun NetworkBadge(@DrawableRes iconResId: Int, colorFilter: ColorFilter?, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(TangemTheme.dimens.size18)
|
||||
.background(
|
||||
color = TangemTheme.colors.background.primary,
|
||||
shape = CircleShape,
|
||||
),
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.padding(all = TangemTheme.dimens.spacing2)
|
||||
.matchParentSize(),
|
||||
painter = painterResource(id = iconResId),
|
||||
colorFilter = colorFilter,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun CustomBadge(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(TangemTheme.dimens.size12)
|
||||
.background(
|
||||
color = TangemTheme.colors.background.primary,
|
||||
shape = CircleShape,
|
||||
),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(all = TangemTheme.dimens.spacing2)
|
||||
.matchParentSize()
|
||||
.background(
|
||||
color = TangemTheme.colors.icon.informative,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package com.tangem.feature.wallet.presentation.common.component.token.icon
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.ColorFilter
|
||||
import androidx.compose.ui.graphics.ColorMatrix
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
|
||||
private const val GRAY_SCALE_SATURATION = 0f
|
||||
|
||||
@Composable
|
||||
internal fun TokenIcon(state: TokenItemState, modifier: Modifier = Modifier) {
|
||||
BaseContainer(modifier = modifier) {
|
||||
val iconModifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(TangemTheme.dimens.size36)
|
||||
|
||||
when (state) {
|
||||
is TokenItemState.Loading -> LoadingIcon(modifier = iconModifier)
|
||||
is TokenItemState.Locked -> LockedIcon(modifier = iconModifier)
|
||||
is TokenItemState.ContentState -> ContentIconContainer(
|
||||
modifier = iconModifier,
|
||||
icon = state.icon,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun LoadingIcon(modifier: Modifier = Modifier) {
|
||||
CircleShimmer(modifier = modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LockedIcon(modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(
|
||||
color = TangemTheme.colors.background.secondary,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.ContentIconContainer(icon: TokenItemState.IconState, modifier: Modifier = Modifier) {
|
||||
val networkBadgeOffset = TangemTheme.dimens.spacing4
|
||||
val colorFilter = remember(icon.isGrayscale) {
|
||||
if (icon.isGrayscale) {
|
||||
ColorFilter.colorMatrix(
|
||||
colorMatrix = ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) },
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
ContentIcon(
|
||||
modifier = modifier,
|
||||
icon = icon,
|
||||
colorFilter = colorFilter,
|
||||
)
|
||||
|
||||
if (icon.networkBadgeIconResId != null) {
|
||||
NetworkBadge(
|
||||
modifier = Modifier
|
||||
.offset(x = networkBadgeOffset, y = -networkBadgeOffset)
|
||||
.align(Alignment.TopEnd),
|
||||
iconResId = requireNotNull(icon.networkBadgeIconResId),
|
||||
colorFilter = colorFilter,
|
||||
)
|
||||
}
|
||||
|
||||
if (icon is TokenItemState.IconState.CustomTokenIcon) {
|
||||
CustomBadge(modifier = Modifier.align(Alignment.BottomEnd))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private inline fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) {
|
||||
Box(modifier = modifier.size(size = TangemTheme.dimens.size40), content = content)
|
||||
}
|
||||
|
|
@ -2,7 +2,9 @@ package com.tangem.feature.wallet.presentation.common.state
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
/** Token item state */
|
||||
@Immutable
|
||||
|
|
@ -18,80 +20,121 @@ internal sealed interface TokenItemState {
|
|||
data class Locked(override val id: String) : TokenItemState
|
||||
|
||||
/** Content state */
|
||||
sealed class ContentState(
|
||||
override val id: String,
|
||||
open val tokenIconUrl: String?,
|
||||
@DrawableRes open val tokenIconResId: Int,
|
||||
@DrawableRes open val networkBadgeIconResId: Int?,
|
||||
open val name: String,
|
||||
) : TokenItemState
|
||||
@Immutable
|
||||
sealed class ContentState : TokenItemState {
|
||||
|
||||
abstract val icon: IconState
|
||||
abstract val name: String
|
||||
}
|
||||
|
||||
/**
|
||||
* Content token state
|
||||
*
|
||||
* @property id unique id
|
||||
* @property tokenIconUrl token icon url
|
||||
* @property tokenIconResId token icon resource id
|
||||
* @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin
|
||||
* @property icon token icon state
|
||||
* @property name token name
|
||||
* @property amount amount of token
|
||||
* @property hasPending pending tx in blockchain
|
||||
* @property tokenOptions state for token options
|
||||
* @property isTestnet indicates whether the token is from test network or not
|
||||
* @property onItemClick callback which will be called when an item is clicked
|
||||
* @property onItemLongClick callback which will be called when an item is long clicked
|
||||
*/
|
||||
data class Content(
|
||||
override val id: String,
|
||||
override val tokenIconUrl: String?,
|
||||
@DrawableRes override val tokenIconResId: Int,
|
||||
@DrawableRes override val networkBadgeIconResId: Int?,
|
||||
override val icon: IconState,
|
||||
override val name: String,
|
||||
val amount: String,
|
||||
val hasPending: Boolean,
|
||||
val tokenOptions: TokenOptionsState,
|
||||
val isTestnet: Boolean,
|
||||
val onItemClick: () -> Unit,
|
||||
val onItemLongClick: () -> Unit,
|
||||
) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name)
|
||||
) : ContentState()
|
||||
|
||||
/**
|
||||
* Draggable token state
|
||||
*
|
||||
* @property id unique id
|
||||
* @property tokenIconUrl token icon url
|
||||
* @property tokenIconResId token icon resource id
|
||||
* @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin
|
||||
* @property icon token icon state
|
||||
* @property name token name
|
||||
* @property fiatAmount fiat amount of token
|
||||
* @property isTestnet indicates whether the token is from test network or not
|
||||
* @property info token info (e.g. fiat balance or status)
|
||||
*/
|
||||
data class Draggable(
|
||||
override val id: String,
|
||||
override val tokenIconUrl: String?,
|
||||
@DrawableRes override val tokenIconResId: Int,
|
||||
@DrawableRes override val networkBadgeIconResId: Int?,
|
||||
override val icon: IconState,
|
||||
override val name: String,
|
||||
val fiatAmount: String,
|
||||
val isTestnet: Boolean,
|
||||
) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name)
|
||||
val info: TextReference,
|
||||
) : ContentState()
|
||||
|
||||
/**
|
||||
* Unreachable token state
|
||||
*
|
||||
* @property id token id
|
||||
* @property tokenIconUrl token icon url
|
||||
* @property tokenIconResId token icon resource id
|
||||
* @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin
|
||||
* @property icon token icon state
|
||||
* @property name token name
|
||||
*/
|
||||
data class Unreachable(
|
||||
override val id: String,
|
||||
override val tokenIconUrl: String?,
|
||||
@DrawableRes override val tokenIconResId: Int,
|
||||
@DrawableRes override val networkBadgeIconResId: Int?,
|
||||
override val icon: IconState,
|
||||
override val name: String,
|
||||
) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name)
|
||||
) : ContentState()
|
||||
|
||||
/**
|
||||
* Represents the various states an icon can be in.
|
||||
*/
|
||||
@Immutable
|
||||
sealed class IconState {
|
||||
|
||||
abstract val networkBadgeIconResId: Int?
|
||||
abstract val isGrayscale: Boolean
|
||||
|
||||
/**
|
||||
* Represents a coin icon.
|
||||
*
|
||||
* @property url The URL where the coin icon can be fetched from. May be `null` if not found.
|
||||
* @property fallbackResId The drawable resource ID to be used as a fallback if the URL is not available.
|
||||
* @property isGrayscale Specifies whether to show the icon in grayscale.
|
||||
*/
|
||||
data class CoinIcon(
|
||||
val url: String?,
|
||||
@DrawableRes val fallbackResId: Int,
|
||||
override val isGrayscale: Boolean,
|
||||
) : IconState() {
|
||||
|
||||
override val networkBadgeIconResId: Int? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a token icon.
|
||||
*
|
||||
* @property url The URL where the token icon can be fetched from. May be `null` if not found.
|
||||
* @property networkBadgeIconResId The drawable resource ID for the network badge.
|
||||
* @property isGrayscale Specifies whether to show the icon in grayscale.
|
||||
* @property fallbackTint The color to be used for tinting the fallback icon.
|
||||
* @property fallbackBackground The background color to be used for the fallback icon.
|
||||
*/
|
||||
data class TokenIcon(
|
||||
val url: String?,
|
||||
@DrawableRes override val networkBadgeIconResId: Int,
|
||||
override val isGrayscale: Boolean,
|
||||
val fallbackTint: Color,
|
||||
val fallbackBackground: Color,
|
||||
) : IconState()
|
||||
|
||||
/**
|
||||
* Represents a custom token icon.
|
||||
*
|
||||
* @property tint The color to be used for tinting the icon.
|
||||
* @property background The background color to be used for the icon.
|
||||
* @property networkBadgeIconResId The drawable resource ID for the network badge.
|
||||
* @property isGrayscale Specifies whether to show the icon in grayscale.
|
||||
*/
|
||||
data class CustomTokenIcon(
|
||||
val tint: Color,
|
||||
val background: Color,
|
||||
@DrawableRes override val networkBadgeIconResId: Int,
|
||||
override val isGrayscale: Boolean,
|
||||
) : IconState()
|
||||
}
|
||||
|
||||
/** Token options state */
|
||||
@Immutable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
package com.tangem.feature.wallet.presentation.common.utils
|
||||
|
||||
import com.tangem.core.ui.extensions.getTintForTokenIcon
|
||||
import com.tangem.core.ui.extensions.networkIconResId
|
||||
import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrencyStatus, TokenItemState.IconState> {
|
||||
|
||||
override fun convert(value: CryptoCurrencyStatus): TokenItemState.IconState {
|
||||
return when (val currency = value.currency) {
|
||||
is CryptoCurrency.Coin -> getIconStateForCoin(currency, value.value.isError)
|
||||
is CryptoCurrency.Token -> getIconStateForToken(currency, value.value.isError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getIconStateForCoin(
|
||||
coin: CryptoCurrency.Coin,
|
||||
isUnreachable: Boolean,
|
||||
): TokenItemState.IconState.CoinIcon {
|
||||
return TokenItemState.IconState.CoinIcon(
|
||||
url = coin.iconUrl,
|
||||
fallbackResId = coin.networkIconResId,
|
||||
isGrayscale = coin.network.isTestnet || isUnreachable,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getIconStateForToken(token: CryptoCurrency.Token, isErrorStatus: Boolean): TokenItemState.IconState {
|
||||
val background = token.tryGetBackgroundForTokenIcon()
|
||||
val tint = getTintForTokenIcon(background)
|
||||
|
||||
return if (token.isCustom) {
|
||||
TokenItemState.IconState.CustomTokenIcon(
|
||||
tint = tint,
|
||||
background = background,
|
||||
networkBadgeIconResId = token.networkIconResId,
|
||||
isGrayscale = token.network.isTestnet || isErrorStatus,
|
||||
)
|
||||
} else {
|
||||
TokenItemState.IconState.TokenIcon(
|
||||
url = token.iconUrl,
|
||||
networkBadgeIconResId = token.networkIconResId,
|
||||
isGrayscale = token.network.isTestnet || isErrorStatus,
|
||||
fallbackTint = tint,
|
||||
fallbackBackground = background,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
package com.tangem.feature.wallet.presentation.organizetokens
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
|
|
@ -19,10 +18,13 @@ import androidx.compose.ui.draw.shadow
|
|||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.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 androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
||||
|
|
@ -37,7 +39,10 @@ import com.tangem.feature.wallet.presentation.common.component.TokenItem
|
|||
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
|
||||
import org.burnoutcrew.reorderable.*
|
||||
import org.burnoutcrew.reorderable.ReorderableItem
|
||||
import org.burnoutcrew.reorderable.ReorderableLazyListState
|
||||
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
|
||||
import org.burnoutcrew.reorderable.reorderable
|
||||
|
||||
@Composable
|
||||
internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier = Modifier) {
|
||||
|
|
@ -91,27 +96,28 @@ private fun TokenList(
|
|||
canDragOver = dndConfig.canDragItemOver,
|
||||
onDragEnd = onDragEnd,
|
||||
)
|
||||
val items = state.items
|
||||
|
||||
val listContentPadding = PaddingValues(
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing92,
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.reorderable(reorderableListState)
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxSize(),
|
||||
.reorderable(reorderableListState),
|
||||
state = reorderableListState.listState,
|
||||
contentPadding = PaddingValues(
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing92,
|
||||
),
|
||||
contentPadding = listContentPadding,
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = items,
|
||||
items = state.items,
|
||||
key = { _, item -> item.id },
|
||||
) { index, item ->
|
||||
|
||||
val onDragStart = remember(item) {
|
||||
{ dndConfig.onDragStart(item) }
|
||||
{ dndConfig.onItemDragStart(item) }
|
||||
}
|
||||
|
||||
DraggableItem(
|
||||
|
|
@ -127,7 +133,6 @@ private fun TokenList(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun LazyItemScope.DraggableItem(
|
||||
index: Int,
|
||||
|
|
@ -135,15 +140,18 @@ private fun LazyItemScope.DraggableItem(
|
|||
reorderableState: ReorderableLazyListState,
|
||||
onDragStart: () -> Unit,
|
||||
) {
|
||||
var isDragging by remember {
|
||||
mutableStateOf(value = false)
|
||||
}
|
||||
|
||||
val itemModifier = Modifier.applyShapeAndShadow(item.roundingMode, item.showShadow)
|
||||
|
||||
ReorderableItem(
|
||||
defaultDraggingModifier = Modifier.animateItemPlacement(
|
||||
animationSpec = tween(easing = LinearOutSlowInEasing),
|
||||
),
|
||||
state = reorderableState,
|
||||
reorderableState = reorderableState,
|
||||
index = index,
|
||||
key = item.id,
|
||||
) { isDragging ->
|
||||
val itemModifier = Modifier.applyShapeAndShadow(item.roundingMode, item.showShadow)
|
||||
) { isItemDragging ->
|
||||
isDragging = isItemDragging
|
||||
|
||||
when (item) {
|
||||
is DraggableItem.GroupHeader -> DraggableNetworkGroupItem(
|
||||
|
|
@ -157,10 +165,12 @@ private fun LazyItemScope.DraggableItem(
|
|||
reorderableTokenListState = reorderableState,
|
||||
)
|
||||
// Should be presented in the list but remain invisible
|
||||
is DraggableItem.GroupPlaceholder -> Box(modifier = Modifier.fillMaxWidth())
|
||||
is DraggableItem.Placeholder -> Box(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(isDragging) {
|
||||
DisposableEffect(isDragging) {
|
||||
onDispose {
|
||||
if (isDragging) {
|
||||
onDragStart()
|
||||
}
|
||||
|
|
@ -283,57 +293,71 @@ private fun Actions(config: OrganizeTokensState.ActionsConfig, modifier: Modifie
|
|||
}
|
||||
}
|
||||
|
||||
private fun Modifier.applyShapeAndShadow(roundingMode: DraggableItem.RoundingMode, showShadow: Boolean): Modifier =
|
||||
composed {
|
||||
private fun Modifier.applyShapeAndShadow(roundingMode: DraggableItem.RoundingMode, showShadow: Boolean): Modifier {
|
||||
return composed {
|
||||
val radius by animateDpAsState(
|
||||
targetValue = if (roundingMode !is DraggableItem.RoundingMode.None) {
|
||||
TangemTheme.dimens.radius16
|
||||
} else {
|
||||
TangemTheme.dimens.radius0
|
||||
targetValue = when (roundingMode) {
|
||||
is DraggableItem.RoundingMode.None -> TangemTheme.dimens.radius0
|
||||
is DraggableItem.RoundingMode.All -> TangemTheme.dimens.radius12
|
||||
is DraggableItem.RoundingMode.Bottom,
|
||||
is DraggableItem.RoundingMode.Top,
|
||||
-> TangemTheme.dimens.radius16
|
||||
},
|
||||
label = "item_shape_radius",
|
||||
)
|
||||
val shape = when (roundingMode) {
|
||||
is DraggableItem.RoundingMode.None -> RectangleShape
|
||||
is DraggableItem.RoundingMode.Top -> RoundedCornerShape(
|
||||
topStart = radius,
|
||||
topEnd = radius,
|
||||
)
|
||||
is DraggableItem.RoundingMode.Bottom -> RoundedCornerShape(
|
||||
bottomStart = radius,
|
||||
bottomEnd = radius,
|
||||
)
|
||||
is DraggableItem.RoundingMode.All -> RoundedCornerShape(
|
||||
size = radius,
|
||||
)
|
||||
}
|
||||
|
||||
val paddingValue = TangemTheme.dimens.spacing4
|
||||
val padding = if (roundingMode.showGap) {
|
||||
when (roundingMode) {
|
||||
is DraggableItem.RoundingMode.None -> null
|
||||
is DraggableItem.RoundingMode.All -> PaddingValues(vertical = paddingValue)
|
||||
is DraggableItem.RoundingMode.Top -> PaddingValues(top = paddingValue)
|
||||
is DraggableItem.RoundingMode.Bottom -> PaddingValues(bottom = paddingValue)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val elevation by animateDpAsState(
|
||||
targetValue = if (showShadow) {
|
||||
TangemTheme.dimens.elevation8
|
||||
} else {
|
||||
TangemTheme.dimens.elevation0
|
||||
},
|
||||
label = "item_elevation",
|
||||
)
|
||||
|
||||
this
|
||||
.let {
|
||||
if (padding != null) {
|
||||
it.padding(padding)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
.padding(paddingValues = getItemGap(roundingMode))
|
||||
.shadow(
|
||||
elevation = if (showShadow) TangemTheme.dimens.elevation12 else TangemTheme.dimens.elevation0,
|
||||
shape = shape,
|
||||
elevation = elevation,
|
||||
shape = getItemShape(roundingMode, radius),
|
||||
clip = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun getItemGap(roundingMode: DraggableItem.RoundingMode): PaddingValues {
|
||||
val paddingValue = TangemTheme.dimens.spacing4
|
||||
|
||||
return if (roundingMode.showGap) {
|
||||
when (roundingMode) {
|
||||
is DraggableItem.RoundingMode.None -> PaddingValues(all = 0.dp)
|
||||
is DraggableItem.RoundingMode.All -> PaddingValues(vertical = paddingValue)
|
||||
is DraggableItem.RoundingMode.Top -> PaddingValues(top = paddingValue)
|
||||
is DraggableItem.RoundingMode.Bottom -> PaddingValues(bottom = paddingValue)
|
||||
}
|
||||
} else {
|
||||
PaddingValues(all = 0.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
private fun getItemShape(roundingMode: DraggableItem.RoundingMode, radius: Dp): Shape {
|
||||
return when (roundingMode) {
|
||||
is DraggableItem.RoundingMode.None -> RectangleShape
|
||||
is DraggableItem.RoundingMode.Top -> RoundedCornerShape(
|
||||
topStart = radius,
|
||||
topEnd = radius,
|
||||
)
|
||||
is DraggableItem.RoundingMode.Bottom -> RoundedCornerShape(
|
||||
bottomStart = radius,
|
||||
bottomEnd = radius,
|
||||
)
|
||||
is DraggableItem.RoundingMode.All -> RoundedCornerShape(
|
||||
size = radius,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ internal class OrganizeTokensStateHolder(
|
|||
),
|
||||
dndConfig = OrganizeTokensState.DragAndDropConfig(
|
||||
onItemDragged = dragAndDropIntents::onItemDragged,
|
||||
onDragStart = dragAndDropIntents::onItemDraggingStart,
|
||||
onItemDragStart = dragAndDropIntents::onItemDraggingStart,
|
||||
onItemDragEnd = dragAndDropIntents::onItemDraggingEnd,
|
||||
canDragItemOver = dragAndDropIntents::canDragItemOver,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -20,13 +20,14 @@ import com.tangem.feature.wallet.presentation.organizetokens.utils.common.disabl
|
|||
import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapter
|
||||
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
|
||||
import com.tangem.feature.wallet.presentation.router.WalletRoute
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@HiltViewModel
|
||||
internal class OrganizeTokensViewModel @Inject constructor(
|
||||
private val getTokenListUseCase: GetTokenListUseCase,
|
||||
|
|
@ -34,6 +35,7 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
private val toggleTokenListSortingUseCase: ToggleTokenListSortingUseCase,
|
||||
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), OrganizeTokensIntents {
|
||||
|
||||
|
|
@ -43,7 +45,6 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
|
||||
private val dragAndDropAdapter = DragAndDropAdapter(
|
||||
listStateProvider = Provider { uiState.value.itemsState },
|
||||
scope = viewModelScope,
|
||||
)
|
||||
|
||||
private val stateHolder = OrganizeTokensStateHolder(
|
||||
|
|
@ -72,7 +73,7 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onSortClick() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
viewModelScope.launch(dispatchers.default) {
|
||||
val list = tokenList ?: return@launch
|
||||
|
||||
toggleTokenListSortingUseCase(list).fold(
|
||||
|
|
@ -86,7 +87,7 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onGroupClick() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
viewModelScope.launch(dispatchers.default) {
|
||||
val list = tokenList ?: return@launch
|
||||
|
||||
toggleTokenListGroupingUseCase(list).fold(
|
||||
|
|
@ -100,7 +101,7 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onApplyClick() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
viewModelScope.launch(dispatchers.default) {
|
||||
stateHolder.updateStateToDisplayProgress()
|
||||
|
||||
val listState = uiState.value.itemsState
|
||||
|
|
@ -117,7 +118,9 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
ifLeft = stateHolder::updateStateWithError,
|
||||
ifRight = {
|
||||
stateHolder.updateStateToHideProgress()
|
||||
withContext(Dispatchers.Main) { router.popBackStack() }
|
||||
withContext(
|
||||
dispatchers.main,
|
||||
) { router.popBackStack() }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -128,9 +131,9 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun bootstrapTokenList() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
viewModelScope.launch(dispatchers.default) {
|
||||
val maybeTokenList = getTokenListUseCase(userWalletId)
|
||||
.first { it.getOrNull()?.totalFiatBalance is TokenList.FiatBalance.Loaded }
|
||||
.first { it.getOrNull()?.totalFiatBalance !is TokenList.FiatBalance.Loading }
|
||||
|
||||
maybeTokenList.fold(
|
||||
ifLeft = stateHolder::updateStateWithError,
|
||||
|
|
|
|||
|
|
@ -51,12 +51,11 @@ internal sealed class DraggableItem {
|
|||
}
|
||||
|
||||
/**
|
||||
* Helper item used to detect possible positions where a network group can be placed.
|
||||
* Used only on [OrganizeTokensListState.GroupedByNetwork] and placed between network groups.
|
||||
* Helper item used to detect possible positions where a draggable item can be placed.
|
||||
*
|
||||
* @property id ID of the placeholder
|
||||
* */
|
||||
data class GroupPlaceholder(
|
||||
data class Placeholder(
|
||||
override val id: String,
|
||||
) : DraggableItem() {
|
||||
override val showShadow: Boolean = false
|
||||
|
|
@ -109,7 +108,7 @@ internal sealed class DraggableItem {
|
|||
* @return updated [DraggableItem]
|
||||
* */
|
||||
fun updateRoundingMode(mode: RoundingMode): DraggableItem = when (this) {
|
||||
is GroupPlaceholder -> this
|
||||
is Placeholder -> this
|
||||
is GroupHeader -> this.copy(roundingMode = mode)
|
||||
is Token -> this.copy(roundingMode = mode)
|
||||
}
|
||||
|
|
@ -122,7 +121,7 @@ internal sealed class DraggableItem {
|
|||
* @return updated [DraggableItem]
|
||||
* */
|
||||
fun updateShadowVisibility(show: Boolean): DraggableItem = when (this) {
|
||||
is GroupPlaceholder -> this
|
||||
is Placeholder -> this
|
||||
is GroupHeader -> this.copy(showShadow = show)
|
||||
is Token -> this.copy(showShadow = show)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ internal sealed class OrganizeTokensListState {
|
|||
) : OrganizeTokensListState()
|
||||
|
||||
data class Ungrouped(
|
||||
override val items: PersistentList<DraggableItem.Token>,
|
||||
override val items: PersistentList<DraggableItem>,
|
||||
) : OrganizeTokensListState()
|
||||
|
||||
object Empty : OrganizeTokensListState() {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,6 @@ internal data class OrganizeTokensState(
|
|||
val onItemDragged: (ItemPosition, ItemPosition) -> Unit,
|
||||
val canDragItemOver: (ItemPosition, ItemPosition) -> Boolean,
|
||||
val onItemDragEnd: () -> Unit,
|
||||
val onDragStart: (DraggableItem) -> Unit,
|
||||
val onItemDragStart: (DraggableItem) -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ internal class CryptoCurrenciesIdsResolver {
|
|||
val draggableTokens = when (listState) {
|
||||
is OrganizeTokensListState.Empty -> return emptyList()
|
||||
is OrganizeTokensListState.GroupedByNetwork -> listState.items.filterIsInstance<DraggableItem.Token>()
|
||||
is OrganizeTokensListState.Ungrouped -> listState.items
|
||||
is OrganizeTokensListState.Ungrouped -> listState.items.filterIsInstance<DraggableItem.Token>()
|
||||
}
|
||||
val currenciesStatuses = when (tokenList) {
|
||||
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.currencies }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common
|
|||
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
|
||||
|
||||
internal fun getGroupPlaceholder(index: Int): DraggableItem.GroupPlaceholder {
|
||||
return DraggableItem.GroupPlaceholder(id = "placeholder_${index.inc()}")
|
||||
internal fun getGroupPlaceholder(index: Int): DraggableItem.Placeholder {
|
||||
return DraggableItem.Placeholder(id = "placeholder_${index.inc()}")
|
||||
}
|
||||
|
|
@ -3,20 +3,22 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common
|
|||
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
|
||||
|
||||
internal fun List<DraggableItem>.uniteItems(): List<DraggableItem> {
|
||||
val lastItemIndex = this.lastIndex
|
||||
val items = prepareItems()
|
||||
val lastItemIndex = items.lastIndex
|
||||
|
||||
return this.mapIndexed { index, item ->
|
||||
return prepareItems().mapIndexed { index, item ->
|
||||
val mode = when (index) {
|
||||
0 -> DraggableItem.RoundingMode.Top()
|
||||
// 1 index is used because the first item is always a placeholder, check `prepareItems()` function
|
||||
1 -> DraggableItem.RoundingMode.Top()
|
||||
lastItemIndex -> DraggableItem.RoundingMode.Bottom()
|
||||
else -> when (item) {
|
||||
is DraggableItem.Placeholder -> DraggableItem.RoundingMode.None
|
||||
is DraggableItem.GroupHeader -> DraggableItem.RoundingMode.Top(showGap = true)
|
||||
is DraggableItem.Token -> if (this[index + 1] is DraggableItem.GroupPlaceholder) {
|
||||
is DraggableItem.Token -> if (items[index + 1] is DraggableItem.Placeholder) {
|
||||
DraggableItem.RoundingMode.Bottom(showGap = true)
|
||||
} else {
|
||||
DraggableItem.RoundingMode.None
|
||||
}
|
||||
is DraggableItem.GroupPlaceholder -> DraggableItem.RoundingMode.None
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -24,4 +26,49 @@ internal fun List<DraggableItem>.uniteItems(): List<DraggableItem> {
|
|||
.updateRoundingMode(mode)
|
||||
.updateShadowVisibility(show = false)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun List<DraggableItem>.divideMovingItem(movingItem: DraggableItem): List<DraggableItem> {
|
||||
val mutableList = this.toMutableList()
|
||||
val listIterator = mutableList.listIterator()
|
||||
|
||||
while (listIterator.hasNext()) {
|
||||
val item = listIterator.next()
|
||||
|
||||
if (item.id == movingItem.id) {
|
||||
val dividedItem = movingItem
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.All())
|
||||
.updateShadowVisibility(show = true)
|
||||
|
||||
listIterator.set(dividedItem)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return mutableList
|
||||
}
|
||||
|
||||
/**
|
||||
* !!! Workaround !!!
|
||||
*
|
||||
* We need to add a [DraggableItem.Placeholder] (since it's not draggable) as the first item of the list, because the
|
||||
* [DND library](https://github.com/aclassen/ComposeReorderable) glitches when a user tries to drag the first item.
|
||||
*
|
||||
* @since 07.09.2023
|
||||
* */
|
||||
private fun List<DraggableItem>.prepareItems(): List<DraggableItem> {
|
||||
val firstPlaceholderId = "initial_placeholder"
|
||||
val items = this
|
||||
|
||||
return mutableListOf<DraggableItem>().apply {
|
||||
add(DraggableItem.Placeholder(firstPlaceholderId))
|
||||
|
||||
val itemsWithoutFirstPlaceholder = if (items.firstOrNull()?.id == firstPlaceholderId) {
|
||||
items.drop(n = 1)
|
||||
} else {
|
||||
items
|
||||
}
|
||||
|
||||
addAll(itemsWithoutFirstPlaceholder)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeToken
|
|||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal inline fun OrganizeTokensListState.updateItems(
|
||||
update: (PersistentList<DraggableItem>) -> List<DraggableItem>,
|
||||
): OrganizeTokensListState {
|
||||
|
|
@ -13,7 +12,7 @@ internal inline fun OrganizeTokensListState.updateItems(
|
|||
|
||||
return when (this) {
|
||||
is OrganizeTokensListState.GroupedByNetwork -> copy(items = updatedItems)
|
||||
is OrganizeTokensListState.Ungrouped -> copy(items = updatedItems as PersistentList<DraggableItem.Token>)
|
||||
is OrganizeTokensListState.Ungrouped -> copy(items = updatedItems)
|
||||
is OrganizeTokensListState.Empty -> this
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.extensions.iconResId
|
||||
import com.tangem.core.ui.extensions.networkBadgeIconResId
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId
|
||||
|
|
@ -16,6 +18,8 @@ internal class CryptoCurrencyToDraggableItemConverter(
|
|||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
) : Converter<CryptoCurrencyStatus, DraggableItem.Token> {
|
||||
|
||||
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
|
||||
|
||||
override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token {
|
||||
return createDraggableToken(value, appCurrencyProvider())
|
||||
}
|
||||
|
|
@ -44,12 +48,13 @@ internal class CryptoCurrencyToDraggableItemConverter(
|
|||
|
||||
return TokenItemState.Draggable(
|
||||
id = getTokenItemId(currency.id),
|
||||
tokenIconUrl = currency.iconUrl,
|
||||
tokenIconResId = currencyStatus.currency.iconResId,
|
||||
networkBadgeIconResId = currencyStatus.currency.networkBadgeIconResId,
|
||||
icon = iconStateConverter.convert(currencyStatus),
|
||||
name = currency.name,
|
||||
fiatAmount = getFormattedFiatAmount(currencyStatus, appCurrency),
|
||||
isTestnet = currencyStatus.currency.network.isTestnet,
|
||||
info = if (currencyStatus.value.isError) {
|
||||
resourceReference(id = R.string.common_unreachable)
|
||||
} else {
|
||||
stringReference(getFormattedFiatAmount(currencyStatus, appCurrency))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,6 @@ internal class NetworkGroupToDraggableItemsConverter(
|
|||
)
|
||||
|
||||
private fun createTokens(group: NetworkGroup): List<DraggableItem.Token> {
|
||||
return itemConverter.convertList(group.currencies.toList())
|
||||
return itemConverter.convertList(group.currencies)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,21 +4,17 @@ import com.tangem.common.Provider
|
|||
import com.tangem.feature.wallet.presentation.organizetokens.DragAndDropIntents
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.updateItems
|
||||
import kotlinx.collections.immutable.mutate
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.burnoutcrew.reorderable.ItemPosition
|
||||
|
||||
internal class DragAndDropAdapter(
|
||||
private val listStateProvider: Provider<OrganizeTokensListState>,
|
||||
private val scope: CoroutineScope,
|
||||
) : DragAndDropIntents {
|
||||
|
||||
private val draggableGroupsOperations = DraggableGroupsOperations()
|
||||
|
|
@ -37,9 +33,12 @@ internal class DragAndDropAdapter(
|
|||
get() = listStateFlowInternal
|
||||
|
||||
override fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean {
|
||||
val items = (currentListState as? OrganizeTokensListState.GroupedByNetwork)
|
||||
?.items
|
||||
?: return true // If ungrouped then item can be moved anywhere
|
||||
val items = when (val listState = currentListState) {
|
||||
is OrganizeTokensListState.GroupedByNetwork -> listState.items
|
||||
is OrganizeTokensListState.Empty,
|
||||
is OrganizeTokensListState.Ungrouped,
|
||||
-> return true // If ungrouped then item can be moved anywhere
|
||||
}
|
||||
|
||||
val (dragOverItem, draggingItem) = findItemsToMove(
|
||||
items = items,
|
||||
|
|
@ -54,7 +53,7 @@ internal class DragAndDropAdapter(
|
|||
return when (draggingItem) {
|
||||
is DraggableItem.GroupHeader -> checkCanMoveHeaderOver(dragOver, dragOverItem, items.lastIndex)
|
||||
is DraggableItem.Token -> checkCanMoveTokenOver(draggingItem, dragOverItem)
|
||||
is DraggableItem.GroupPlaceholder -> false
|
||||
is DraggableItem.Placeholder -> false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -64,11 +63,11 @@ internal class DragAndDropAdapter(
|
|||
|
||||
updateListState {
|
||||
when (item) {
|
||||
is DraggableItem.GroupPlaceholder -> items
|
||||
is DraggableItem.Placeholder -> items
|
||||
is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroup(items, item)
|
||||
is DraggableItem.Token -> when (this) {
|
||||
is OrganizeTokensListState.GroupedByNetwork -> draggableGroupsOperations.divideGroups(items, item)
|
||||
is OrganizeTokensListState.Ungrouped -> divideTokens(items, item)
|
||||
is OrganizeTokensListState.GroupedByNetwork -> items.divideMovingItem(item)
|
||||
is OrganizeTokensListState.Ungrouped -> items.divideMovingItem(item)
|
||||
is OrganizeTokensListState.Empty -> items
|
||||
}
|
||||
}
|
||||
|
|
@ -76,21 +75,17 @@ internal class DragAndDropAdapter(
|
|||
}
|
||||
|
||||
override fun onItemDraggingEnd() {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val draggingItem = currentDraggingItem ?: return@launch
|
||||
val draggingItem = currentDraggingItem ?: return
|
||||
|
||||
delay(FINISH_DRAGGING_DELAY_MILLIS)
|
||||
|
||||
updateListState {
|
||||
when (draggingItem) {
|
||||
is DraggableItem.GroupHeader -> draggableGroupsOperations.expandGroups(items)
|
||||
is DraggableItem.Token -> items.uniteItems()
|
||||
is DraggableItem.GroupPlaceholder -> items
|
||||
}
|
||||
updateListState {
|
||||
when (draggingItem) {
|
||||
is DraggableItem.GroupHeader -> draggableGroupsOperations.expandGroups(items)
|
||||
is DraggableItem.Token -> items.uniteItems()
|
||||
is DraggableItem.Placeholder -> items
|
||||
}
|
||||
|
||||
currentDraggingItem = null
|
||||
}
|
||||
|
||||
currentDraggingItem = null
|
||||
}
|
||||
|
||||
override fun onItemDragged(from: ItemPosition, to: ItemPosition) = updateListState {
|
||||
|
|
@ -137,7 +132,7 @@ internal class DragAndDropAdapter(
|
|||
return when {
|
||||
moveOverItemPosition.index == 0 -> true
|
||||
moveOverItemPosition.index == lastItemIndex -> true
|
||||
moveOverItem is DraggableItem.GroupPlaceholder -> true
|
||||
moveOverItem is DraggableItem.Placeholder -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
|
@ -147,23 +142,7 @@ internal class DragAndDropAdapter(
|
|||
return when (moveOverItem) {
|
||||
is DraggableItem.GroupHeader -> false // Token item can not be moved to group item
|
||||
is DraggableItem.Token -> item.groupId == moveOverItem.groupId // Token item can not be moved over its group
|
||||
is DraggableItem.GroupPlaceholder -> false
|
||||
is DraggableItem.Placeholder -> false
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST") // Erased type
|
||||
private fun divideTokens(
|
||||
items: List<DraggableItem.Token>,
|
||||
movingItem: DraggableItem.Token,
|
||||
): List<DraggableItem.Token> {
|
||||
return items.map { token ->
|
||||
token
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true))
|
||||
.updateShadowVisibility(show = token.id == movingItem.id)
|
||||
} as List<DraggableItem.Token>
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val FINISH_DRAGGING_DELAY_MILLIS = 200L
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd
|
||||
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ internal class DraggableGroupsOperations {
|
|||
it is DraggableItem.Token && it.groupId == movingGroup.id
|
||||
}
|
||||
|
||||
return divideGroups(itemsWithoutGroupTokens, movingGroup)
|
||||
return itemsWithoutGroupTokens.divideMovingItem(movingGroup)
|
||||
}
|
||||
|
||||
fun expandGroups(items: List<DraggableItem>): List<DraggableItem> {
|
||||
|
|
@ -45,62 +46,4 @@ internal class DraggableGroupsOperations {
|
|||
|
||||
return expandedGroups
|
||||
}
|
||||
|
||||
fun divideGroups(items: List<DraggableItem>, movingItem: DraggableItem): List<DraggableItem> {
|
||||
val lastItemIndex = items.lastIndex
|
||||
|
||||
return items.mapIndexed { index, item ->
|
||||
when {
|
||||
// Case when current item is the moving item
|
||||
item.id == movingItem.id -> {
|
||||
item
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true))
|
||||
.updateShadowVisibility(show = true)
|
||||
}
|
||||
// Case when moving item is a token and current item is the group of the moving token
|
||||
movingItem is DraggableItem.Token && item.id == movingItem.groupId -> {
|
||||
item
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true))
|
||||
.updateShadowVisibility(show = true)
|
||||
}
|
||||
// Case when both moving item and current item are tokens and belong to the same group
|
||||
movingItem is DraggableItem.Token &&
|
||||
item is DraggableItem.Token && item.groupId == movingItem.groupId -> {
|
||||
item
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true))
|
||||
.updateShadowVisibility(show = false)
|
||||
}
|
||||
// Case when current item is the first item in the list
|
||||
index == 0 -> {
|
||||
item
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.Top())
|
||||
.updateShadowVisibility(show = false)
|
||||
}
|
||||
// Case when current item is the last item in the list
|
||||
index == lastItemIndex -> {
|
||||
item
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.Bottom())
|
||||
.updateShadowVisibility(show = false)
|
||||
}
|
||||
// Case when previous item is a GroupPlaceholder
|
||||
items[index - 1] is DraggableItem.GroupPlaceholder -> {
|
||||
item
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.Top(showGap = true))
|
||||
.updateShadowVisibility(show = false)
|
||||
}
|
||||
// Case when next item is a GroupPlaceholder
|
||||
items[index + 1] is DraggableItem.GroupPlaceholder -> {
|
||||
item
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.Bottom(showGap = true))
|
||||
.updateShadowVisibility(show = false)
|
||||
}
|
||||
// Default case when none of the above conditions are met
|
||||
else -> {
|
||||
item
|
||||
.updateRoundingMode(DraggableItem.RoundingMode.None)
|
||||
.updateShadowVisibility(show = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import com.tangem.core.ui.components.transactions.state.TransactionState
|
|||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.*
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
|
|
@ -19,7 +20,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
internal sealed class WalletSingleCurrencyState : WalletState.ContentState() {
|
||||
|
||||
/** Manage buttons */
|
||||
abstract val buttons: ImmutableList<WalletManageButton>
|
||||
abstract val buttons: PersistentList<WalletManageButton>
|
||||
|
||||
/** Transactions history state */
|
||||
abstract val txHistoryState: TxHistoryState
|
||||
|
|
@ -31,7 +32,7 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() {
|
|||
override val pullToRefreshConfig: WalletPullToRefreshConfig,
|
||||
override val notifications: ImmutableList<WalletNotification>,
|
||||
override val bottomSheetConfig: WalletBottomSheetConfig?,
|
||||
override val buttons: ImmutableList<WalletManageButton>,
|
||||
override val buttons: PersistentList<WalletManageButton>,
|
||||
override val txHistoryState: TxHistoryState,
|
||||
val marketPriceBlockState: MarketPriceBlockState,
|
||||
) : WalletSingleCurrencyState()
|
||||
|
|
@ -41,7 +42,7 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() {
|
|||
override val topBarConfig: WalletTopBarConfig,
|
||||
override val walletsListConfig: WalletsListConfig,
|
||||
override val pullToRefreshConfig: WalletPullToRefreshConfig,
|
||||
override val buttons: ImmutableList<WalletManageButton>,
|
||||
override val buttons: PersistentList<WalletManageButton>,
|
||||
override val onUnlockWalletsNotificationClick: () -> Unit,
|
||||
override val onUnlockClick: () -> Unit,
|
||||
override val onScanClick: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.feature.wallet.impl.R
|
|||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import javax.annotation.concurrent.Immutable
|
||||
|
||||
/**
|
||||
* Wallet tokens list state
|
||||
|
|
@ -20,11 +21,10 @@ internal sealed class WalletTokensListState {
|
|||
* Wallet content token list state
|
||||
*
|
||||
* @property items content items
|
||||
* @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked
|
||||
*/
|
||||
sealed class ContentState(
|
||||
open val items: ImmutableList<TokensListItemState>,
|
||||
open val onOrganizeTokensClick: (() -> Unit)?,
|
||||
open val organizeTokensButton: OrganizeTokensButtonState,
|
||||
) : WalletTokensListState()
|
||||
|
||||
/**
|
||||
|
|
@ -37,18 +37,18 @@ internal sealed class WalletTokensListState {
|
|||
TokensListItemState.Token(state = TokenItemState.Loading(id = FIRST_LOADING_TOKEN_ID)),
|
||||
TokensListItemState.Token(state = TokenItemState.Loading(id = SECOND_LOADING_TOKEN_ID)),
|
||||
),
|
||||
) : ContentState(items = items, onOrganizeTokensClick = null)
|
||||
) : ContentState(items = items, organizeTokensButton = OrganizeTokensButtonState.Hidden)
|
||||
|
||||
/**
|
||||
* Content state
|
||||
*
|
||||
* @property items content items
|
||||
* @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked
|
||||
* @property organizeTokensButton represents the state of the 'Organize Tokens' button
|
||||
*/
|
||||
data class Content(
|
||||
override val items: ImmutableList<TokensListItemState>,
|
||||
override val onOrganizeTokensClick: (() -> Unit)?,
|
||||
) : ContentState(items, onOrganizeTokensClick)
|
||||
override val organizeTokensButton: OrganizeTokensButtonState,
|
||||
) : ContentState(items, organizeTokensButton)
|
||||
|
||||
/** Locked content state */
|
||||
object Locked : ContentState(
|
||||
|
|
@ -56,10 +56,32 @@ internal sealed class WalletTokensListState {
|
|||
TokensListItemState.NetworkGroupTitle(value = TextReference.Res(id = R.string.main_tokens)),
|
||||
TokensListItemState.Token(state = TokenItemState.Locked(id = LOCKED_TOKEN_ID)),
|
||||
),
|
||||
onOrganizeTokensClick = null,
|
||||
organizeTokensButton = OrganizeTokensButtonState.Hidden,
|
||||
)
|
||||
|
||||
/**
|
||||
* Represents the state of the 'Organize Tokens' button.
|
||||
*/
|
||||
@Immutable
|
||||
sealed class OrganizeTokensButtonState {
|
||||
|
||||
/** Represents the state where the 'Organize Tokens' button is hidden. */
|
||||
object Hidden : OrganizeTokensButtonState()
|
||||
|
||||
/**
|
||||
* Represents the state where the 'Organize Tokens' button is visible.
|
||||
*
|
||||
* @property isEnabled Indicates if the button is enabled or not.
|
||||
* @property onClick Callback to be executed when the button is clicked.
|
||||
*/
|
||||
data class Visible(
|
||||
val isEnabled: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
) : OrganizeTokensButtonState()
|
||||
}
|
||||
|
||||
/** Tokens list item state */
|
||||
@Immutable
|
||||
sealed interface TokensListItemState {
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,36 +1,32 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.factory
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Converter from loaded [TokenItemState.Content] to ImmutableList<[TokenActionButtonConfig]>
|
||||
*
|
||||
* @property currentStateProvider current ui state provider
|
||||
* @property clickIntents screen click intents
|
||||
*
|
||||
*/
|
||||
@Suppress("UnusedPrivateMember")
|
||||
internal class TokenActionsProvider(
|
||||
private val currentStateProvider: Provider<WalletState>,
|
||||
) {
|
||||
internal class TokenActionsProvider(private val clickIntents: WalletClickIntents) {
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
fun provideActions(tokenId: String): ImmutableList<TokenActionButtonConfig> {
|
||||
fun provideActions(cryptoCurrencyStatus: CryptoCurrencyStatus): ImmutableList<TokenActionButtonConfig> {
|
||||
// TODO: [REDACTED_JIRA]
|
||||
return mockTokenActionButtonConfig().toImmutableList()
|
||||
return mockTokenActionButtonConfig(cryptoCurrencyStatus).toImmutableList()
|
||||
}
|
||||
|
||||
private fun mockTokenActionButtonConfig(): List<TokenActionButtonConfig> {
|
||||
private fun mockTokenActionButtonConfig(cryptoCurrencyStatus: CryptoCurrencyStatus): List<TokenActionButtonConfig> {
|
||||
return listOf(
|
||||
TokenActionButtonConfig(
|
||||
text = "Send",
|
||||
iconResId = R.drawable.ic_plus_24,
|
||||
onClick = {},
|
||||
onClick = { clickIntents.onMultiCurrencySendClick(cryptoCurrencyStatus) },
|
||||
),
|
||||
TokenActionButtonConfig(
|
||||
text = "Buy",
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class WalletCryptoCurrencyActionsConverter(
|
||||
private val currentStateProvider: Provider<WalletState>,
|
||||
|
|
@ -26,7 +26,7 @@ internal class WalletCryptoCurrencyActionsConverter(
|
|||
}
|
||||
}
|
||||
|
||||
private fun List<TokenActionsState.ActionState>.mapToManageButtons(): ImmutableList<WalletManageButton> {
|
||||
private fun List<TokenActionsState.ActionState>.mapToManageButtons(): PersistentList<WalletManageButton> {
|
||||
return this
|
||||
.mapNotNull { action ->
|
||||
when (action) {
|
||||
|
|
@ -40,11 +40,14 @@ internal class WalletCryptoCurrencyActionsConverter(
|
|||
WalletManageButton.Sell(enabled = action.enabled, onClick = clickIntents::onSellClick)
|
||||
}
|
||||
is TokenActionsState.ActionState.Send -> {
|
||||
WalletManageButton.Send(enabled = action.enabled, onClick = clickIntents::onSendClick)
|
||||
WalletManageButton.Send(
|
||||
enabled = action.enabled,
|
||||
onClick = clickIntents::onSingleCurrencySendClick,
|
||||
)
|
||||
}
|
||||
is TokenActionsState.ActionState.Swap -> null
|
||||
}
|
||||
}
|
||||
.toImmutableList()
|
||||
.toPersistentList()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.factory
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletDeleteStateConverter.DeleteWalletModel
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletsUpdateActionResolver
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Converter that responds on wallet deleting action. Returns [WalletState] without deleted wallet.
|
||||
*
|
||||
* @property currentStateProvider current state provider
|
||||
*/
|
||||
internal class WalletDeleteStateConverter(
|
||||
private val currentStateProvider: Provider<WalletState>,
|
||||
) : Converter<DeleteWalletModel, WalletState> {
|
||||
|
||||
override fun convert(value: DeleteWalletModel): WalletState {
|
||||
return when (val state = currentStateProvider()) {
|
||||
is WalletState.ContentState -> {
|
||||
value.cacheState.copySealed(
|
||||
walletsListConfig = state.walletsListConfig.copy(
|
||||
selectedWalletIndex = value.action.selectedWalletIndex,
|
||||
wallets = state.walletsListConfig.wallets.deleteWallet(id = value.action.deletedWalletId),
|
||||
),
|
||||
pullToRefreshConfig = value.cacheState.pullToRefreshConfig.copy(isRefreshing = false),
|
||||
)
|
||||
}
|
||||
is WalletState.Initial -> state
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<WalletCardState>.deleteWallet(id: UserWalletId): ImmutableList<WalletCardState> {
|
||||
return this
|
||||
.mapIndexedNotNull { index, currentWallet ->
|
||||
if (currentWallet.id == id) return@mapIndexedNotNull null
|
||||
getOrNull(index) ?: return@mapIndexedNotNull null
|
||||
}
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
data class DeleteWalletModel(
|
||||
val cacheState: WalletState.ContentState,
|
||||
val action: WalletsUpdateActionResolver.Action.DeleteWallet,
|
||||
)
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ import com.tangem.domain.tokens.model.TokenList
|
|||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletLoadedTokensListConverter.LoadedTokensListModel
|
||||
import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorConverter
|
||||
import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
|
|
@ -27,11 +26,12 @@ import com.tangem.utils.converter.Converter
|
|||
*/
|
||||
internal class WalletLoadedTokensListConverter(
|
||||
private val currentStateProvider: Provider<WalletState>,
|
||||
private val tokenListErrorConverter: TokenListErrorConverter,
|
||||
appCurrencyProvider: Provider<AppCurrency>,
|
||||
cardTypeResolverProvider: Provider<CardTypesResolver>,
|
||||
currentWalletProvider: Provider<UserWallet>,
|
||||
clickIntents: WalletClickIntents,
|
||||
) : Converter<LoadedTokensListModel, WalletState> {
|
||||
) : Converter<Either<TokenListError, TokenList>, WalletState> {
|
||||
|
||||
private val tokenListStateConverter = TokenListToWalletStateConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
|
|
@ -42,26 +42,10 @@ internal class WalletLoadedTokensListConverter(
|
|||
clickIntents = clickIntents,
|
||||
)
|
||||
|
||||
private val tokenListErrorStateConverter = TokenListErrorConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
)
|
||||
|
||||
override fun convert(value: LoadedTokensListModel): WalletState {
|
||||
return value.tokenListEither.fold(
|
||||
ifLeft = tokenListErrorStateConverter::convert,
|
||||
ifRight = {
|
||||
tokenListStateConverter.convert(
|
||||
value = TokenListToWalletStateConverter.TokensListModel(
|
||||
tokenList = it,
|
||||
isRefreshing = value.isRefreshing,
|
||||
),
|
||||
)
|
||||
},
|
||||
override fun convert(value: Either<TokenListError, TokenList>): WalletState {
|
||||
return value.fold(
|
||||
ifLeft = tokenListErrorConverter::convert,
|
||||
ifRight = tokenListStateConverter::convert,
|
||||
)
|
||||
}
|
||||
|
||||
data class LoadedTokensListModel(
|
||||
val tokenListEither: Either<TokenListError, TokenList>,
|
||||
val isRefreshing: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,110 +1,78 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.factory
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class WalletLockedConverter(
|
||||
private val currentStateProvider: Provider<WalletState>,
|
||||
private val currentCardTypeResolverProvider: Provider<CardTypesResolver>,
|
||||
private val currentWalletProvider: Provider<UserWallet>,
|
||||
private val clickIntents: WalletClickIntents,
|
||||
) : Converter<Unit, WalletState> {
|
||||
|
||||
override fun convert(value: Unit): WalletState {
|
||||
return when (val state = currentStateProvider()) {
|
||||
is WalletState.ContentState -> {
|
||||
val cardTypeResolver = currentCardTypeResolverProvider()
|
||||
|
||||
if (cardTypeResolver.isMultiwalletAllowed()) {
|
||||
state.toMultiCurrencyLockedState(cardTypeResolver)
|
||||
} else {
|
||||
state.toSingleCurrencyLockedState(cardTypeResolver)
|
||||
}
|
||||
}
|
||||
is WalletState.Initial -> state
|
||||
is WalletMultiCurrencyState.Content -> state.toMultiCurrencyLockedState()
|
||||
is WalletSingleCurrencyState.Content -> state.toSingleCurrencyLockedState()
|
||||
is WalletMultiCurrencyState.Locked,
|
||||
is WalletSingleCurrencyState.Locked,
|
||||
is WalletState.Initial,
|
||||
-> state
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletState.ContentState.toMultiCurrencyLockedState(
|
||||
cardTypeResolver: CardTypesResolver,
|
||||
): WalletMultiCurrencyState.Locked {
|
||||
private fun WalletMultiCurrencyState.Content.toMultiCurrencyLockedState(): WalletState {
|
||||
return WalletMultiCurrencyState.Locked(
|
||||
onBackClick = onBackClick,
|
||||
topBarConfig = createTopBarConfig(),
|
||||
walletsListConfig = createWalletsListConfig(cardTypeResolver),
|
||||
pullToRefreshConfig = pullToRefreshConfig,
|
||||
topBarConfig = topBarConfig.updateCallback(),
|
||||
walletsListConfig = walletsListConfig,
|
||||
pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(),
|
||||
onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick,
|
||||
onUnlockClick = clickIntents::onUnlockWalletClick,
|
||||
onScanClick = clickIntents::onScanCardClick,
|
||||
onScanClick = clickIntents::onScanToUnlockWalletClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletState.ContentState.toSingleCurrencyLockedState(
|
||||
cardTypeResolver: CardTypesResolver,
|
||||
): WalletSingleCurrencyState.Locked {
|
||||
private fun WalletSingleCurrencyState.Content.toSingleCurrencyLockedState(): WalletState {
|
||||
return WalletSingleCurrencyState.Locked(
|
||||
onBackClick = onBackClick,
|
||||
topBarConfig = createTopBarConfig(),
|
||||
walletsListConfig = createWalletsListConfig(cardTypeResolver),
|
||||
pullToRefreshConfig = pullToRefreshConfig,
|
||||
buttons = createButtons(),
|
||||
topBarConfig = topBarConfig.updateCallback(),
|
||||
walletsListConfig = walletsListConfig,
|
||||
pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(),
|
||||
buttons = buttons.disableButtons(),
|
||||
onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick,
|
||||
onUnlockClick = clickIntents::onUnlockWalletClick,
|
||||
onScanClick = clickIntents::onScanCardClick,
|
||||
onScanClick = clickIntents::onScanToUnlockWalletClick,
|
||||
onExploreClick = clickIntents::onExploreClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletState.ContentState.createTopBarConfig(): WalletTopBarConfig {
|
||||
return topBarConfig.copy(onMoreClick = clickIntents::onUnlockWalletNotificationClick)
|
||||
private fun WalletTopBarConfig.updateCallback(): WalletTopBarConfig {
|
||||
return copy(onMoreClick = clickIntents::onUnlockWalletNotificationClick)
|
||||
}
|
||||
|
||||
private fun WalletState.ContentState.createWalletsListConfig(
|
||||
cardTypeResolver: CardTypesResolver,
|
||||
): WalletsListConfig {
|
||||
return walletsListConfig.copy(
|
||||
wallets = walletsListConfig.wallets
|
||||
.map { walletCardState ->
|
||||
WalletCardState.LockedContent(
|
||||
id = walletCardState.id,
|
||||
title = walletCardState.title,
|
||||
additionalInfo = if (cardTypeResolver.isMultiwalletAllowed()) {
|
||||
WalletAdditionalInfoFactory.resolve(
|
||||
cardTypesResolver = cardTypeResolver,
|
||||
wallet = currentWalletProvider(),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
imageResId = walletCardState.imageResId,
|
||||
onRenameClick = walletCardState.onRenameClick,
|
||||
onDeleteClick = walletCardState.onDeleteClick,
|
||||
)
|
||||
private fun WalletPullToRefreshConfig.stopRefreshing(): WalletPullToRefreshConfig {
|
||||
return copy(isRefreshing = false)
|
||||
}
|
||||
|
||||
private fun PersistentList<WalletManageButton>.disableButtons(): PersistentList<WalletManageButton> {
|
||||
return this
|
||||
.map { button ->
|
||||
when (button) {
|
||||
is WalletManageButton.Buy -> button.copy(enabled = false)
|
||||
is WalletManageButton.Sell -> button.copy(enabled = false)
|
||||
is WalletManageButton.Send -> button.copy(enabled = false)
|
||||
is WalletManageButton.Swap -> button.copy(enabled = false)
|
||||
is WalletManageButton.Receive -> button
|
||||
}
|
||||
.toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createButtons(): ImmutableList<WalletManageButton> {
|
||||
return persistentListOf(
|
||||
WalletManageButton.Buy(enabled = false, onClick = {}),
|
||||
WalletManageButton.Send(enabled = false, onClick = {}),
|
||||
WalletManageButton.Receive(onClick = {}),
|
||||
WalletManageButton.Sell(enabled = false, onClick = {}),
|
||||
)
|
||||
}
|
||||
.toPersistentList()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,124 +1,115 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.factory
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.*
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.mutate
|
||||
|
||||
internal class WalletRefreshStateConverter(
|
||||
private val currentStateProvider: Provider<WalletState>,
|
||||
private val currentCardTypeResolverProvider: Provider<CardTypesResolver>,
|
||||
private val clickIntents: WalletClickIntents,
|
||||
) : Converter<Unit, WalletState> {
|
||||
) : Converter<Boolean, WalletState> {
|
||||
|
||||
override fun convert(value: Unit): WalletState {
|
||||
return when (val state = currentStateProvider()) {
|
||||
is WalletMultiCurrencyState.Content -> state.getRefreshState()
|
||||
is WalletSingleCurrencyState.Content -> state.getRefreshState()
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
override fun convert(value: Boolean): WalletState {
|
||||
val state = currentStateProvider()
|
||||
val contentState = state as? WalletState.ContentState ?: return state
|
||||
|
||||
private fun WalletMultiCurrencyState.Content.getRefreshState(): WalletMultiCurrencyState.Content {
|
||||
return copy(
|
||||
walletsListConfig = createWalletsListConfig(),
|
||||
pullToRefreshConfig = createPullToRefreshConfig(),
|
||||
tokensListState = createTokenListState(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletSingleCurrencyState.Content.getRefreshState(): WalletSingleCurrencyState.Content {
|
||||
return copy(
|
||||
walletsListConfig = createWalletsListConfig(),
|
||||
pullToRefreshConfig = createPullToRefreshConfig(),
|
||||
buttons = buttons.mapToDisabledButton(),
|
||||
txHistoryState = createTxHistoryState(),
|
||||
marketPriceBlockState = MarketPriceBlockState.Loading(currencyName = marketPriceBlockState.currencyName),
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletState.ContentState.createWalletsListConfig(): WalletsListConfig {
|
||||
val selectedWallet = walletsListConfig.wallets[walletsListConfig.selectedWalletIndex]
|
||||
val additionalInfo = if (currentCardTypeResolverProvider().isMultiwalletAllowed()) {
|
||||
selectedWallet.additionalInfo
|
||||
return if (value) {
|
||||
contentState.getRefreshingState()
|
||||
} else {
|
||||
null
|
||||
contentState.getRefreshedState()
|
||||
}
|
||||
}
|
||||
|
||||
return walletsListConfig.copy(
|
||||
wallets = walletsListConfig.wallets.toPersistentList().set(
|
||||
index = walletsListConfig.selectedWalletIndex,
|
||||
element = WalletCardState.Loading(
|
||||
id = selectedWallet.id,
|
||||
title = selectedWallet.title,
|
||||
additionalInfo = additionalInfo,
|
||||
imageResId = selectedWallet.imageResId,
|
||||
onRenameClick = selectedWallet.onRenameClick,
|
||||
onDeleteClick = selectedWallet.onDeleteClick,
|
||||
),
|
||||
),
|
||||
private fun WalletState.ContentState.getRefreshingState(): WalletState {
|
||||
return when (this) {
|
||||
is WalletMultiCurrencyState.Content -> getRefreshingState()
|
||||
is WalletSingleCurrencyState.Content -> getRefreshingState()
|
||||
is WalletMultiCurrencyState.Locked,
|
||||
is WalletSingleCurrencyState.Locked,
|
||||
-> this
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletState.ContentState.getRefreshedState(): WalletState {
|
||||
return when (this) {
|
||||
is WalletMultiCurrencyState.Content -> getRefreshedState()
|
||||
is WalletSingleCurrencyState.Content -> getRefreshedState()
|
||||
is WalletMultiCurrencyState.Locked,
|
||||
is WalletSingleCurrencyState.Locked,
|
||||
-> this
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletMultiCurrencyState.Content.getRefreshingState(): WalletMultiCurrencyState {
|
||||
return copy(
|
||||
pullToRefreshConfig = updatePullToRefreshConfig(isRefreshing = true),
|
||||
tokensListState = updateTokenListState(isRefreshing = true),
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletState.ContentState.createPullToRefreshConfig(): WalletPullToRefreshConfig {
|
||||
return pullToRefreshConfig.copy(isRefreshing = true)
|
||||
private fun WalletSingleCurrencyState.Content.getRefreshingState(): WalletSingleCurrencyState {
|
||||
return copy(
|
||||
pullToRefreshConfig = updatePullToRefreshConfig(isRefreshing = true),
|
||||
buttons = updateButtons(isRefreshing = true),
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletMultiCurrencyState.Content.createTokenListState(): WalletTokensListState {
|
||||
return when (tokensListState) {
|
||||
private fun WalletMultiCurrencyState.Content.getRefreshedState(): WalletMultiCurrencyState {
|
||||
return copy(
|
||||
pullToRefreshConfig = updatePullToRefreshConfig(isRefreshing = false),
|
||||
tokensListState = updateTokenListState(isRefreshing = false),
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletSingleCurrencyState.Content.getRefreshedState(): WalletSingleCurrencyState {
|
||||
return copy(
|
||||
pullToRefreshConfig = updatePullToRefreshConfig(isRefreshing = false),
|
||||
buttons = updateButtons(isRefreshing = false),
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletMultiCurrencyState.updateTokenListState(isRefreshing: Boolean): WalletTokensListState {
|
||||
return when (val listState = tokensListState) {
|
||||
is WalletTokensListState.Content -> {
|
||||
WalletTokensListState.Loading(
|
||||
items = tokensListState.items
|
||||
.filterIsInstance<TokensListItemState.Token>()
|
||||
.mapToLoadingTokenState(),
|
||||
)
|
||||
when (listState.organizeTokensButton) {
|
||||
is WalletTokensListState.OrganizeTokensButtonState.Hidden -> listState
|
||||
is WalletTokensListState.OrganizeTokensButtonState.Visible -> listState.copy(
|
||||
organizeTokensButton = listState.organizeTokensButton.copy(
|
||||
isEnabled = !isRefreshing,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
is WalletTokensListState.Empty -> WalletTokensListState.Loading()
|
||||
is WalletTokensListState.Loading,
|
||||
is WalletTokensListState.Locked,
|
||||
-> tokensListState
|
||||
is WalletTokensListState.Loading,
|
||||
is WalletTokensListState.Empty,
|
||||
-> listState
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<TokensListItemState.Token>.mapToLoadingTokenState(): ImmutableList<TokensListItemState.Token> {
|
||||
return this
|
||||
.map { TokensListItemState.Token(state = TokenItemState.Loading(id = it.state.id)) }
|
||||
.toImmutableList()
|
||||
}
|
||||
private fun WalletSingleCurrencyState.updateButtons(isRefreshing: Boolean): PersistentList<WalletManageButton> {
|
||||
val isButtonsEnabled = !isRefreshing
|
||||
|
||||
private fun ImmutableList<WalletManageButton>.mapToDisabledButton(): ImmutableList<WalletManageButton> {
|
||||
return this
|
||||
.mapNotNull { button ->
|
||||
return buttons.mutate {
|
||||
it.mapNotNull { button ->
|
||||
when (button) {
|
||||
is WalletManageButton.Buy -> button.copy(enabled = false)
|
||||
is WalletManageButton.Send -> button.copy(enabled = false)
|
||||
is WalletManageButton.Buy -> button.copy(enabled = isButtonsEnabled)
|
||||
is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled)
|
||||
is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled)
|
||||
is WalletManageButton.Receive -> button
|
||||
is WalletManageButton.Sell -> button.copy(enabled = false)
|
||||
is WalletManageButton.Swap -> null
|
||||
}
|
||||
}
|
||||
.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletSingleCurrencyState.Content.createTxHistoryState(): TxHistoryState {
|
||||
if (txHistoryState is TxHistoryState.Content) {
|
||||
txHistoryState.contentItems.update {
|
||||
TxHistoryState.getDefaultLoadingTransactions(onExploreClick = clickIntents::onExploreClick)
|
||||
}
|
||||
}
|
||||
|
||||
return txHistoryState
|
||||
private fun WalletState.ContentState.updatePullToRefreshConfig(isRefreshing: Boolean): WalletPullToRefreshConfig {
|
||||
return pullToRefreshConfig.copy(isRefreshing = isRefreshing)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.factory
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal class WalletRenameStateConverter(
|
||||
private val currentStateProvider: Provider<WalletState>,
|
||||
) : Converter<String, WalletState> {
|
||||
|
||||
override fun convert(value: String): WalletState {
|
||||
return when (val state = currentStateProvider()) {
|
||||
is WalletState.ContentState -> {
|
||||
state.copySealed(
|
||||
walletsListConfig = state.walletsListConfig.renameSelectedWallet(name = value),
|
||||
)
|
||||
}
|
||||
is WalletState.Initial -> state
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletsListConfig.renameSelectedWallet(name: String): WalletsListConfig {
|
||||
return copy(
|
||||
wallets = wallets
|
||||
.mapIndexed { index, walletCard ->
|
||||
if (index == selectedWalletIndex) walletCard.copySealed(title = name) else walletCard
|
||||
}
|
||||
.toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,11 +11,12 @@ import com.tangem.domain.tokens.error.CurrencyStatusError
|
|||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletSingleCurrencyLoadedBalanceConverter.SingleCurrencyLoadedBalanceModel
|
||||
import com.tangem.feature.wallet.presentation.wallet.utils.CurrencyStatusErrorConverter
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -25,34 +26,32 @@ internal class WalletSingleCurrencyLoadedBalanceConverter(
|
|||
private val cardTypeResolverProvider: Provider<CardTypesResolver>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val currentWalletProvider: Provider<UserWallet>,
|
||||
) : Converter<SingleCurrencyLoadedBalanceModel, WalletSingleCurrencyState.Content> {
|
||||
private val currencyStatusErrorConverter: CurrencyStatusErrorConverter,
|
||||
) : Converter<Either<CurrencyStatusError, CryptoCurrencyStatus>, WalletState> {
|
||||
|
||||
override fun convert(value: SingleCurrencyLoadedBalanceModel): WalletSingleCurrencyState.Content {
|
||||
return value.cryptoCurrencyEither.fold(
|
||||
ifLeft = { convertError() },
|
||||
ifRight = { convertContent(it, value.isRefreshing) },
|
||||
override fun convert(value: Either<CurrencyStatusError, CryptoCurrencyStatus>): WalletState {
|
||||
return value.fold(
|
||||
ifLeft = currencyStatusErrorConverter::convert,
|
||||
ifRight = ::convertContent,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertError(): WalletSingleCurrencyState.Content {
|
||||
return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content)
|
||||
}
|
||||
private fun convertContent(status: CryptoCurrencyStatus): WalletState {
|
||||
return when (val state = currentStateProvider()) {
|
||||
is WalletSingleCurrencyState.Content -> {
|
||||
val currencyName = state.marketPriceBlockState.currencyName
|
||||
|
||||
private fun convertContent(
|
||||
status: CryptoCurrencyStatus,
|
||||
isRefreshing: Boolean,
|
||||
): WalletSingleCurrencyState.Content {
|
||||
val state = requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content)
|
||||
val currencyName = state.marketPriceBlockState.currencyName
|
||||
return state.copy(
|
||||
walletsListConfig = getUpdatedSelectedWallet(status = status.value, state = state),
|
||||
pullToRefreshConfig = if (isRefreshing) {
|
||||
state.pullToRefreshConfig.copy(isRefreshing = status.value is CryptoCurrencyStatus.Loading)
|
||||
} else {
|
||||
state.pullToRefreshConfig
|
||||
},
|
||||
marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName),
|
||||
)
|
||||
state.copy(
|
||||
walletsListConfig = getUpdatedSelectedWallet(status = status.value, state = state),
|
||||
marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName),
|
||||
)
|
||||
}
|
||||
is WalletMultiCurrencyState.Content,
|
||||
is WalletMultiCurrencyState.Locked,
|
||||
is WalletSingleCurrencyState.Locked,
|
||||
is WalletState.Initial,
|
||||
-> state
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMarketPriceState(status: CryptoCurrencyStatus.Status, currencyName: String): MarketPriceBlockState {
|
||||
|
|
@ -168,9 +167,4 @@ internal class WalletSingleCurrencyLoadedBalanceConverter(
|
|||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
|
||||
data class SingleCurrencyLoadedBalanceModel(
|
||||
val cryptoCurrencyEither: Either<CurrencyStatusError, CryptoCurrencyStatus>,
|
||||
val isRefreshing: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.factory
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
|
||||
|
|
@ -14,7 +16,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.*
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletSkeletonStateConverter.SkeletonModel
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -33,12 +35,12 @@ internal class WalletSkeletonStateConverter(
|
|||
) : Converter<SkeletonModel, WalletState.ContentState> {
|
||||
|
||||
override fun convert(value: SkeletonModel): WalletState.ContentState {
|
||||
val cardTypeResolver = value.wallets[value.selectedWalletIndex].scanResponse.cardTypesResolver
|
||||
val selectedWallet = value.wallets[value.selectedWalletIndex]
|
||||
|
||||
return if (cardTypeResolver.isMultiwalletAllowed()) {
|
||||
return if (selectedWallet.isMultiCurrency) {
|
||||
createMultiCurrencyState(value = value)
|
||||
} else {
|
||||
createSingleCurrencyState(value = value, currencyName = cardTypeResolver.getBlockchain().currency)
|
||||
createSingleCurrencyState(value = value, currencyName = selectedWallet.getPrimaryCurrencyName())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,6 +76,10 @@ internal class WalletSkeletonStateConverter(
|
|||
)
|
||||
}
|
||||
|
||||
private fun UserWallet.getPrimaryCurrencyName(): String {
|
||||
return scanResponse.cardTypesResolver.getBlockchain().currency
|
||||
}
|
||||
|
||||
private fun createTopBarConfig(): WalletTopBarConfig {
|
||||
return WalletTopBarConfig(
|
||||
onScanCardClick = clickIntents::onScanCardClick,
|
||||
|
|
@ -84,51 +90,68 @@ internal class WalletSkeletonStateConverter(
|
|||
private fun createWalletsListConfig(value: SkeletonModel): WalletsListConfig {
|
||||
return WalletsListConfig(
|
||||
selectedWalletIndex = value.selectedWalletIndex,
|
||||
wallets = value.wallets.map(::createWalletState).toImmutableList(),
|
||||
wallets = value.wallets.mapIndexed(::createWalletCardState).toImmutableList(),
|
||||
onWalletChange = clickIntents::onWalletChange,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createWalletState(wallet: UserWallet): WalletCardState {
|
||||
val state = currentStateProvider()
|
||||
|
||||
// If it isn't first initialization (example, when user unlocks wallet)
|
||||
return if (state is WalletState.ContentState) {
|
||||
val initializedWallet = state.walletsListConfig.wallets.first { it.id == wallet.walletId }
|
||||
|
||||
// If wallet is initialized, return it, otherwise return loading state
|
||||
if (initializedWallet !is WalletCardState.Loading) {
|
||||
initializedWallet.copySealed(title = wallet.name)
|
||||
} else {
|
||||
createWalletLoadingState(wallet)
|
||||
}
|
||||
} else {
|
||||
createWalletLoadingState(wallet)
|
||||
}
|
||||
/**
|
||||
* Create wallet card state by [index] and [wallet].
|
||||
* If current wallet card state is initialized, then method returns it.
|
||||
* Otherwise, returns loading wallet card state.
|
||||
*/
|
||||
private fun createWalletCardState(index: Int, wallet: UserWallet): WalletCardState {
|
||||
return currentStateProvider().getInitializedWalletCardState(index) ?: wallet.mapToWalletCardState()
|
||||
}
|
||||
|
||||
private fun createWalletLoadingState(wallet: UserWallet): WalletCardState {
|
||||
val cardTypeResolver = wallet.scanResponse.cardTypesResolver
|
||||
private fun WalletState.getInitializedWalletCardState(index: Int): WalletCardState? {
|
||||
return (this as? WalletState.ContentState)?.walletsListConfig?.wallets?.getOrNull(index)
|
||||
}
|
||||
|
||||
return WalletCardState.Loading(
|
||||
id = wallet.walletId,
|
||||
title = wallet.name,
|
||||
additionalInfo = if (cardTypeResolver.isMultiwalletAllowed()) {
|
||||
WalletAdditionalInfoFactory.resolve(cardTypesResolver = cardTypeResolver, wallet = wallet)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
imageResId = WalletImageResolver.resolve(cardTypesResolver = cardTypeResolver),
|
||||
private fun UserWallet.mapToWalletCardState(): WalletCardState {
|
||||
return if (isLocked) mapToLockedWalletCardState() else mapToLoadingWalletCardState()
|
||||
}
|
||||
|
||||
private fun UserWallet.mapToLockedWalletCardState(): WalletCardState {
|
||||
return WalletCardState.LockedContent(
|
||||
id = walletId,
|
||||
title = name,
|
||||
additionalInfo = createAdditionalInfo(),
|
||||
imageResId = createImageResId(),
|
||||
onRenameClick = clickIntents::onRenameClick,
|
||||
onDeleteClick = clickIntents::onDeleteClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun UserWallet.mapToLoadingWalletCardState(): WalletCardState {
|
||||
return WalletCardState.Loading(
|
||||
id = walletId,
|
||||
title = name,
|
||||
additionalInfo = createAdditionalInfo(),
|
||||
imageResId = createImageResId(),
|
||||
onRenameClick = clickIntents::onRenameClick,
|
||||
onDeleteClick = clickIntents::onDeleteClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun UserWallet.createAdditionalInfo(): TextReference? {
|
||||
return if (isMultiCurrency) {
|
||||
WalletAdditionalInfoFactory.resolve(cardTypesResolver = scanResponse.cardTypesResolver, wallet = this)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
private fun UserWallet.createImageResId(): Int? {
|
||||
return WalletImageResolver.resolve(cardTypesResolver = scanResponse.cardTypesResolver)
|
||||
}
|
||||
|
||||
private fun createPullToRefreshConfig(): WalletPullToRefreshConfig {
|
||||
return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = clickIntents::onRefreshSwipe)
|
||||
}
|
||||
|
||||
private fun createButtons(): ImmutableList<WalletManageButton> {
|
||||
private fun createButtons(): PersistentList<WalletManageButton> {
|
||||
return persistentListOf(
|
||||
WalletManageButton.Buy(enabled = false, onClick = {}),
|
||||
WalletManageButton.Send(enabled = false, onClick = {}),
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBott
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadedTxHistoryConverter
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadingTxHistoryConverter
|
||||
import com.tangem.feature.wallet.presentation.wallet.utils.CurrencyStatusErrorConverter
|
||||
import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorConverter
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletsUpdateActionResolver
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
|
@ -42,12 +45,26 @@ internal class WalletStateFactory(
|
|||
private val clickIntents: WalletClickIntents,
|
||||
) {
|
||||
|
||||
private val tokenActionsProvider by lazy { TokenActionsProvider(currentStateProvider = currentStateProvider) }
|
||||
private val tokenActionsProvider by lazy { TokenActionsProvider(clickIntents) }
|
||||
|
||||
private val skeletonConverter by lazy { WalletSkeletonStateConverter(currentStateProvider, clickIntents) }
|
||||
|
||||
private val walletsUnlockStateConverter by lazy { WalletsUnlockStateConverter(currentStateProvider, clickIntents) }
|
||||
|
||||
private val walletRenameStateConverter by lazy { WalletRenameStateConverter(currentStateProvider) }
|
||||
|
||||
private val walletDeleteStateConverter by lazy { WalletDeleteStateConverter(currentStateProvider) }
|
||||
|
||||
private val tokenListErrorConverter by lazy {
|
||||
TokenListErrorConverter(currentStateProvider)
|
||||
}
|
||||
private val currencyStatusErrorConverter by lazy {
|
||||
CurrencyStatusErrorConverter(currentStateProvider)
|
||||
}
|
||||
private val loadedTokensListConverter by lazy {
|
||||
WalletLoadedTokensListConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
tokenListErrorConverter = tokenListErrorConverter,
|
||||
cardTypeResolverProvider = currentCardTypeResolverProvider,
|
||||
currentWalletProvider = currentWalletProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
|
|
@ -76,24 +93,19 @@ internal class WalletStateFactory(
|
|||
cardTypeResolverProvider = currentCardTypeResolverProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
currentWalletProvider = currentWalletProvider,
|
||||
currencyStatusErrorConverter = currencyStatusErrorConverter,
|
||||
)
|
||||
}
|
||||
|
||||
private val lockedConverter by lazy {
|
||||
WalletLockedConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
currentCardTypeResolverProvider = currentCardTypeResolverProvider,
|
||||
currentWalletProvider = currentWalletProvider,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
}
|
||||
|
||||
private val refreshStateConverter by lazy {
|
||||
WalletRefreshStateConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
currentCardTypeResolverProvider = currentCardTypeResolverProvider,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
WalletRefreshStateConverter(currentStateProvider)
|
||||
}
|
||||
|
||||
private val cryptoCurrencyActionsConverter by lazy {
|
||||
|
|
@ -114,15 +126,29 @@ internal class WalletStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun getStateByTokensList(tokenListEither: Either<TokenListError, TokenList>, isRefreshing: Boolean): WalletState {
|
||||
return loadedTokensListConverter.convert(
|
||||
value = WalletLoadedTokensListConverter.LoadedTokensListModel(
|
||||
tokenListEither = tokenListEither,
|
||||
isRefreshing = isRefreshing,
|
||||
),
|
||||
fun getStateWithUpdatedWalletName(name: String): WalletState = walletRenameStateConverter.convert(value = name)
|
||||
|
||||
fun getUnlockedState(action: WalletsUpdateActionResolver.Action.UnlockWallet): WalletState {
|
||||
return walletsUnlockStateConverter.convert(value = action)
|
||||
}
|
||||
|
||||
fun getStateWithoutDeletedWallet(
|
||||
cacheState: WalletState.ContentState,
|
||||
action: WalletsUpdateActionResolver.Action.DeleteWallet,
|
||||
): WalletState {
|
||||
return walletDeleteStateConverter.convert(
|
||||
value = WalletDeleteStateConverter.DeleteWalletModel(cacheState = cacheState, action = action),
|
||||
)
|
||||
}
|
||||
|
||||
fun getStateByTokensList(maybeTokenList: Either<TokenListError, TokenList>): WalletState {
|
||||
return loadedTokensListConverter.convert(maybeTokenList)
|
||||
}
|
||||
|
||||
fun getStateByTokenListError(error: TokenListError): WalletState {
|
||||
return tokenListErrorConverter.convert(error)
|
||||
}
|
||||
|
||||
fun getStateByNotifications(notifications: ImmutableList<WalletNotification>): WalletState {
|
||||
return when (val state = currentStateProvider()) {
|
||||
is WalletMultiCurrencyState.Content -> state.copy(notifications = notifications)
|
||||
|
|
@ -131,7 +157,9 @@ internal class WalletStateFactory(
|
|||
}
|
||||
}
|
||||
|
||||
fun getStateAfterContentRefreshing(): WalletState = refreshStateConverter.convert(Unit)
|
||||
fun getRefreshingState(): WalletState = refreshStateConverter.convert(value = true)
|
||||
|
||||
fun getRefreshedState(): WalletState = refreshStateConverter.convert(value = false)
|
||||
|
||||
fun getStateWithOpenWalletBottomSheet(content: WalletBottomSheetConfig.BottomSheetContentConfig): WalletState {
|
||||
return when (val state = currentStateProvider() as WalletState.ContentState) {
|
||||
|
|
@ -157,6 +185,7 @@ internal class WalletStateFactory(
|
|||
isBottomSheetShow = true,
|
||||
onBottomSheetDismiss = clickIntents::onDismissBottomSheet,
|
||||
)
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -170,15 +199,16 @@ internal class WalletStateFactory(
|
|||
bottomSheetConfig = state.bottomSheetConfig?.copy(isShow = false),
|
||||
)
|
||||
is WalletSingleCurrencyState.Locked -> state.copy(isBottomSheetShow = false)
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
|
||||
fun getStateWithTokenActionBottomSheet(tokenId: String): WalletState {
|
||||
fun getStateWithTokenActionBottomSheet(currencyStatus: CryptoCurrencyStatus): WalletState {
|
||||
return when (val state = currentStateProvider() as WalletState.ContentState) {
|
||||
is WalletMultiCurrencyState.Content -> state.copy(
|
||||
tokenActionsBottomSheet = ActionsBottomSheetConfig(
|
||||
isShow = true,
|
||||
actions = tokenActionsProvider.provideActions(tokenId = tokenId),
|
||||
actions = tokenActionsProvider.provideActions(currencyStatus),
|
||||
onDismissRequest = clickIntents::onDismissActionsBottomSheet,
|
||||
),
|
||||
)
|
||||
|
|
@ -199,18 +229,16 @@ internal class WalletStateFactory(
|
|||
fun getLockedState(): WalletState = lockedConverter.convert(Unit)
|
||||
|
||||
fun getSingleCurrencyLoadedBalanceState(
|
||||
cryptoCurrencyEither: Either<CurrencyStatusError, CryptoCurrencyStatus>,
|
||||
isRefreshing: Boolean,
|
||||
maybeCryptoCurrencyStatus: Either<CurrencyStatusError, CryptoCurrencyStatus>,
|
||||
): WalletState {
|
||||
return singleCurrencyLoadedBalanceConverter.convert(
|
||||
value = WalletSingleCurrencyLoadedBalanceConverter.SingleCurrencyLoadedBalanceModel(
|
||||
cryptoCurrencyEither = cryptoCurrencyEither,
|
||||
isRefreshing = isRefreshing,
|
||||
),
|
||||
)
|
||||
return singleCurrencyLoadedBalanceConverter.convert(maybeCryptoCurrencyStatus)
|
||||
}
|
||||
|
||||
fun getSingleCurrencyManageButtonsState(actions: List<TokenActionsState.ActionState>): WalletState {
|
||||
return cryptoCurrencyActionsConverter.convert(value = actions)
|
||||
}
|
||||
|
||||
fun getStateByCurrencyStatusError(error: CurrencyStatusError): WalletState {
|
||||
return currencyStatusErrorConverter.convert(error)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.factory
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
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.wallet.domain.WalletAdditionalInfoFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.*
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletsUpdateActionResolver.Action.UnlockWallet as UnlockWalletAction
|
||||
|
||||
/**
|
||||
* Converter that responds on wallets unlocking action. Returns [WalletState] with unlocked wallets.
|
||||
*
|
||||
* @property currentStateProvider current ui state provider
|
||||
* @property clickIntents screen click intents
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class WalletsUnlockStateConverter(
|
||||
private val currentStateProvider: Provider<WalletState>,
|
||||
private val clickIntents: WalletClickIntents,
|
||||
) : Converter<UnlockWalletAction, WalletState> {
|
||||
|
||||
override fun convert(value: UnlockWalletAction): WalletState {
|
||||
return when (val state = currentStateProvider()) {
|
||||
is WalletMultiCurrencyState.Locked -> state.toMultiCurrencyContentState(value)
|
||||
is WalletSingleCurrencyState.Locked -> state.toSingleCurrencyContentState(value)
|
||||
is WalletState.Initial,
|
||||
is WalletMultiCurrencyState.Content,
|
||||
is WalletSingleCurrencyState.Content,
|
||||
-> state
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletMultiCurrencyState.Locked.toMultiCurrencyContentState(action: UnlockWalletAction): WalletState {
|
||||
return WalletMultiCurrencyState.Content(
|
||||
onBackClick = onBackClick,
|
||||
topBarConfig = topBarConfig.updateCallback(),
|
||||
walletsListConfig = walletsListConfig.unlockWallets(action),
|
||||
pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(),
|
||||
tokensListState = WalletTokensListState.Loading(),
|
||||
notifications = persistentListOf(),
|
||||
bottomSheetConfig = null,
|
||||
tokenActionsBottomSheet = null,
|
||||
onManageTokensClick = clickIntents::onManageTokensClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletSingleCurrencyState.Locked.toSingleCurrencyContentState(action: UnlockWalletAction): WalletState {
|
||||
return WalletSingleCurrencyState.Content(
|
||||
onBackClick = onBackClick,
|
||||
topBarConfig = topBarConfig.updateCallback(),
|
||||
walletsListConfig = walletsListConfig.unlockWallets(action),
|
||||
pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(),
|
||||
notifications = persistentListOf(),
|
||||
bottomSheetConfig = null,
|
||||
buttons = buttons,
|
||||
marketPriceBlockState = MarketPriceBlockState.Loading(
|
||||
currencyName = action.selectedWallet.getPrimaryCurrencyName(),
|
||||
),
|
||||
txHistoryState = TxHistoryState.Content(
|
||||
contentItems = MutableStateFlow(
|
||||
value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletTopBarConfig.updateCallback(): WalletTopBarConfig {
|
||||
return copy(onMoreClick = clickIntents::onDetailsClick)
|
||||
}
|
||||
|
||||
private fun WalletsListConfig.unlockWallets(action: UnlockWalletAction): WalletsListConfig {
|
||||
return this.copy(
|
||||
selectedWalletIndex = action.selectedWalletIndex,
|
||||
wallets = wallets.unlockWallets(action),
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<WalletCardState>.unlockWallets(action: UnlockWalletAction): ImmutableList<WalletCardState> {
|
||||
return this
|
||||
.map { prevWallet ->
|
||||
if (prevWallet is WalletCardState.LockedContent && action.isUnlockedWallet(prevWallet.id)) {
|
||||
prevWallet.mapToLoadingWalletCardState(
|
||||
userWallet = action.getUnlockWallet(prevWallet.id),
|
||||
)
|
||||
} else {
|
||||
prevWallet
|
||||
}
|
||||
}
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
private fun UnlockWalletAction.isUnlockedWallet(walletId: UserWalletId): Boolean {
|
||||
return unlockedWallets.any { it.walletId == walletId }
|
||||
}
|
||||
|
||||
private fun UnlockWalletAction.getUnlockWallet(walletId: UserWalletId): UserWallet {
|
||||
return unlockedWallets.firstOrNull { it.walletId == walletId }
|
||||
?: error("Unlocked wallet with id $walletId not found")
|
||||
}
|
||||
|
||||
private fun WalletCardState.mapToLoadingWalletCardState(userWallet: UserWallet): WalletCardState {
|
||||
return WalletCardState.Loading(
|
||||
id = id,
|
||||
title = title,
|
||||
additionalInfo = userWallet.createAdditionalInfo(),
|
||||
imageResId = WalletImageResolver.resolve(cardTypesResolver = userWallet.scanResponse.cardTypesResolver),
|
||||
onRenameClick = onRenameClick,
|
||||
onDeleteClick = onDeleteClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun UserWallet.createAdditionalInfo(): TextReference? {
|
||||
return if (isMultiCurrency) {
|
||||
WalletAdditionalInfoFactory.resolve(cardTypesResolver = scanResponse.cardTypesResolver, wallet = this)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletPullToRefreshConfig.stopRefreshing(): WalletPullToRefreshConfig {
|
||||
return copy(isRefreshing = false)
|
||||
}
|
||||
|
||||
private fun UserWallet.getPrimaryCurrencyName(): String {
|
||||
return scanResponse.cardTypesResolver.getBlockchain().currency
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
|||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.models.TxHistoryListError
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
|
|
@ -41,18 +42,34 @@ internal class WalletLoadedTxHistoryConverter(
|
|||
}
|
||||
|
||||
private fun convertError(error: TxHistoryListError): WalletState {
|
||||
return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy(
|
||||
txHistoryState = when (error) {
|
||||
is TxHistoryListError.DataError -> {
|
||||
TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick)
|
||||
}
|
||||
},
|
||||
)
|
||||
return when (val state = currentStateProvider()) {
|
||||
is WalletSingleCurrencyState.Content -> {
|
||||
state.copy(
|
||||
txHistoryState = when (error) {
|
||||
is TxHistoryListError.DataError -> {
|
||||
TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
is WalletMultiCurrencyState.Content,
|
||||
is WalletMultiCurrencyState.Locked,
|
||||
is WalletSingleCurrencyState.Locked,
|
||||
is WalletState.Initial,
|
||||
-> state
|
||||
}
|
||||
}
|
||||
|
||||
private fun convert(items: Flow<PagingData<TxHistoryItem>>): WalletState {
|
||||
return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy(
|
||||
txHistoryState = walletTxHistoryItemFlowConverter.convert(value = items),
|
||||
)
|
||||
return when (val state = currentStateProvider()) {
|
||||
is WalletSingleCurrencyState.Content -> {
|
||||
state.copy(txHistoryState = walletTxHistoryItemFlowConverter.convert(value = items))
|
||||
}
|
||||
is WalletMultiCurrencyState.Content,
|
||||
is WalletMultiCurrencyState.Locked,
|
||||
is WalletSingleCurrencyState.Locked,
|
||||
is WalletState.Initial,
|
||||
-> state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -51,11 +51,11 @@ internal class WalletLoadingTxHistoryConverter(
|
|||
}
|
||||
}
|
||||
|
||||
private fun convert(value: Int): WalletSingleCurrencyState.Content {
|
||||
val state = requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content)
|
||||
val txHistoryContent = requireNotNull(state.txHistoryState as? Content)
|
||||
private fun convert(value: Int): WalletState {
|
||||
val state = currentStateProvider()
|
||||
val txHistoryContent = (state as? WalletSingleCurrencyState.Content)?.txHistoryState as? Content
|
||||
|
||||
txHistoryContent.contentItems.update {
|
||||
txHistoryContent?.contentItems?.update {
|
||||
PagingData.from(
|
||||
data = listOf(TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) +
|
||||
MutableList(
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
@ -26,10 +26,11 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencySt
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.OrganizeTokensButtonState
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.common.*
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeButton
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.controlButtons
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
|
||||
|
|
@ -110,9 +111,15 @@ private fun WalletContent(state: WalletState.ContentState) {
|
|||
contentItems(state = state, txHistoryItems = txHistoryItems, modifier = movableItemModifier)
|
||||
|
||||
if (state is WalletMultiCurrencyState) {
|
||||
val tokensListState = state.tokensListState
|
||||
if (tokensListState is WalletTokensListState.ContentState) {
|
||||
organizeButton(onClick = tokensListState.onOrganizeTokensClick, modifier = itemModifier)
|
||||
val contentTokenListState = state.tokensListState as? WalletTokensListState.ContentState
|
||||
val organizeTokensButton = contentTokenListState?.organizeTokensButton
|
||||
|
||||
if (organizeTokensButton is OrganizeTokensButtonState.Visible) {
|
||||
organizeTokensButton(
|
||||
modifier = itemModifier,
|
||||
isEnabled = organizeTokensButton.isEnabled,
|
||||
onClick = organizeTokensButton.onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ private fun ActionsBottomSheetContent_Dark(
|
|||
@PreviewParameter(ActionsBottomSheetContentConfigProvider::class)
|
||||
config: ActionsBottomSheetConfig,
|
||||
) {
|
||||
TangemTheme(isDark = false) {
|
||||
TangemTheme(isDark = true) {
|
||||
// Use preview of content because ModalBottomSheet isn't supported in Preview mode
|
||||
ActionsBottomSheetContent(actions = config.actions)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ 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 androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
|
@ -163,22 +164,14 @@ private fun CardContainer(
|
|||
|
||||
var isRenameWalletDialogVisible by rememberSaveable { mutableStateOf(value = false) }
|
||||
|
||||
DropdownMenu(
|
||||
expanded = isMenuVisible,
|
||||
ManageWalletContextMenu(
|
||||
isMenuVisible = isMenuVisible,
|
||||
pressOffset = pressOffset,
|
||||
itemHeight = itemHeight,
|
||||
onDismissRequest = { isMenuVisible = false },
|
||||
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
|
||||
offset = pressOffset.copy(y = pressOffset.y - itemHeight),
|
||||
) {
|
||||
MenuItem(
|
||||
textResId = R.string.common_rename,
|
||||
imageVector = Icons.Outlined.Edit,
|
||||
onClick = {
|
||||
isMenuVisible = false
|
||||
isRenameWalletDialogVisible = true
|
||||
},
|
||||
)
|
||||
MenuItem(textResId = R.string.common_delete, imageVector = Icons.Outlined.Delete, onClick = onDeleteClick)
|
||||
}
|
||||
onShowRenameWalletDialogClick = { isRenameWalletDialogVisible = true },
|
||||
onDeleteClick = onDeleteClick,
|
||||
)
|
||||
|
||||
if (isRenameWalletDialogVisible) {
|
||||
RenameWalletDialogContent(
|
||||
|
|
@ -192,6 +185,41 @@ private fun CardContainer(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun ManageWalletContextMenu(
|
||||
isMenuVisible: Boolean,
|
||||
pressOffset: DpOffset,
|
||||
itemHeight: Dp,
|
||||
onDismissRequest: () -> Unit,
|
||||
onShowRenameWalletDialogClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = isMenuVisible,
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
|
||||
offset = pressOffset.copy(y = pressOffset.y - itemHeight),
|
||||
) {
|
||||
MenuItem(
|
||||
textResId = R.string.common_rename,
|
||||
imageVector = Icons.Outlined.Edit,
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onShowRenameWalletDialogClick()
|
||||
},
|
||||
)
|
||||
MenuItem(
|
||||
textResId = R.string.common_delete,
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onDeleteClick()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MenuItem(@StringRes textResId: Int, imageVector: ImageVector, onClick: () -> Unit) {
|
||||
DropdownMenuItem(
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollec
|
|||
@Composable
|
||||
internal fun WalletSideEffects(lazyListState: LazyListState, walletsListConfig: WalletsListConfig) {
|
||||
LaunchedEffect(key1 = walletsListConfig.selectedWalletIndex) {
|
||||
lazyListState.scrollToItem(walletsListConfig.selectedWalletIndex)
|
||||
if (!lazyListState.isScrollInProgress) {
|
||||
lazyListState.animateScrollToItem(walletsListConfig.selectedWalletIndex)
|
||||
}
|
||||
}
|
||||
|
||||
val dragInteraction = lazyListState.interactionSource.interactions.collectAsState(initial = null)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
||||
import com.tangem.core.ui.components.buttons.actions.RoundedActionButton
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
|
||||
private const val ORGANIZE_BUTTON_CONTENT_TYPE = "OrganizeTokensButton"
|
||||
|
||||
|
|
@ -14,9 +17,20 @@ private const val ORGANIZE_BUTTON_CONTENT_TYPE = "OrganizeTokensButton"
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal fun LazyListScope.organizeButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) {
|
||||
internal fun LazyListScope.organizeTokensButton(
|
||||
isEnabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
item(key = ORGANIZE_BUTTON_CONTENT_TYPE, contentType = ORGANIZE_BUTTON_CONTENT_TYPE) {
|
||||
OrganizeTokensButton(onClick = onClick, modifier = modifier.animateItemPlacement())
|
||||
RoundedActionButton(
|
||||
modifier = modifier,
|
||||
config = ActionButtonConfig(
|
||||
text = resourceReference(id = R.string.organize_tokens_title),
|
||||
iconResId = R.drawable.ic_filter_24,
|
||||
onClick = onClick,
|
||||
enabled = isEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
||||
import com.tangem.core.ui.components.buttons.actions.RoundedActionButton
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
|
||||
/**
|
||||
* Organize tokens button
|
||||
*
|
||||
* @param onClick callback, if null button is disabled
|
||||
* @param modifier modifier
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun OrganizeTokensButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) {
|
||||
RoundedActionButton(
|
||||
config = ActionButtonConfig(
|
||||
text = TextReference.Res(id = R.string.organize_tokens_title),
|
||||
iconResId = R.drawable.ic_filter_24,
|
||||
onClick = onClick ?: {},
|
||||
enabled = onClick != null,
|
||||
),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui.utils
|
||||
|
||||
import androidx.compose.foundation.interaction.DragInteraction
|
||||
import androidx.compose.foundation.interaction.Interaction
|
||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
|
|
@ -27,7 +28,8 @@ internal class ScrollOffsetCollector(
|
|||
private val LazyListItemInfo.halfItemSize get() = size.div(other = 2)
|
||||
|
||||
override suspend fun emit(value: List<LazyListItemInfo>) {
|
||||
if (!lazyListState.isScrollInProgress || dragInteraction.value == null || value.size <= 1) return
|
||||
if (isNotUserInteraction() || value.size <= 1) return
|
||||
|
||||
val firstItem = value.firstOrNull() ?: return
|
||||
val lastItem = value.lastOrNull() ?: return
|
||||
|
||||
|
|
@ -37,4 +39,12 @@ internal class ScrollOffsetCollector(
|
|||
callback(lastItem.index - 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sometimes the list is scrolled programmatically. Example: selecting a specific wallet when a user opens the
|
||||
* screen for the first time or scans a new wallet. Therefore [ScrollOffsetCollector] should not respond to changes.
|
||||
*/
|
||||
private fun isNotUserInteraction(): Boolean {
|
||||
return !lazyListState.isScrollInProgress || dragInteraction.value !is DragInteraction.Start
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,11 @@ package com.tangem.feature.wallet.presentation.wallet.utils
|
|||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeConfig
|
||||
import com.tangem.core.ui.extensions.iconResId
|
||||
import com.tangem.core.ui.extensions.networkBadgeIconResId
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -18,6 +17,8 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
|
|||
private val clickIntents: WalletClickIntents,
|
||||
) : Converter<CryptoCurrencyStatus, TokenItemState> {
|
||||
|
||||
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
|
||||
|
||||
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
|
||||
return when (value.value) {
|
||||
is CryptoCurrencyStatus.Loading -> TokenItemState.Loading(id = value.currency.id.value)
|
||||
|
|
@ -37,9 +38,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
|
|||
return TokenItemState.Content(
|
||||
id = currency.id.value,
|
||||
name = currency.name,
|
||||
tokenIconUrl = currency.iconUrl,
|
||||
tokenIconResId = currency.iconResId,
|
||||
networkBadgeIconResId = currency.networkBadgeIconResId,
|
||||
icon = iconStateConverter.convert(value = this),
|
||||
amount = getFormattedAmount(),
|
||||
hasPending = value.hasCurrentNetworkTransactions,
|
||||
tokenOptions = if (isWalletContentHidden) {
|
||||
|
|
@ -50,9 +49,8 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
|
|||
config = getPriceChangeConfig(),
|
||||
)
|
||||
},
|
||||
isTestnet = currency.network.isTestnet,
|
||||
onItemClick = { clickIntents.onTokenItemClick(currency) },
|
||||
onItemLongClick = { clickIntents.onTokenItemLongClick(currency) },
|
||||
onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -72,9 +70,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
|
|||
private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable(
|
||||
id = currency.id.value,
|
||||
name = currency.name,
|
||||
tokenIconUrl = currency.iconUrl,
|
||||
tokenIconResId = currency.iconResId,
|
||||
networkBadgeIconResId = currency.networkBadgeIconResId,
|
||||
icon = iconStateConverter.convert(value = this),
|
||||
)
|
||||
|
||||
private fun CryptoCurrencyStatus.getPriceChangeConfig(): PriceChangeConfig {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.utils
|
||||
|
||||
import com.tangem.common.Converter
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
|
||||
// TODO: Implement this
|
||||
internal class CurrencyStatusErrorConverter(
|
||||
private val currentStateProvider: Provider<WalletState>,
|
||||
) : Converter<CurrencyStatusError, WalletSingleCurrencyState.Content> {
|
||||
|
||||
override fun convert(value: CurrencyStatusError): WalletSingleCurrencyState.Content {
|
||||
return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ internal class TokenListErrorConverter(
|
|||
state.copy(
|
||||
tokensListState = WalletTokensListState.Content(
|
||||
items = persistentListOf(),
|
||||
onOrganizeTokensClick = null,
|
||||
organizeTokensButton = WalletTokensListState.OrganizeTokensButtonState.Hidden,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
|||
import com.tangem.domain.tokens.model.NetworkGroup
|
||||
import com.tangem.domain.tokens.model.TokenList
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.OrganizeTokensButtonState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState
|
||||
import com.tangem.feature.wallet.presentation.wallet.utils.LoadingItemsProvider.getLoadingMultiCurrencyTokens
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
|
@ -28,26 +28,15 @@ internal class TokenListToContentItemsConverter(
|
|||
)
|
||||
|
||||
override fun convert(value: TokenList): WalletTokensListState {
|
||||
val isEmptyList = when (value) {
|
||||
is TokenList.GroupedByNetwork -> value.groups.isEmpty()
|
||||
is TokenList.NotInitialized -> false
|
||||
is TokenList.Ungrouped -> value.currencies.isEmpty()
|
||||
}
|
||||
|
||||
return if (isEmptyList) {
|
||||
WalletTokensListState.Empty
|
||||
} else {
|
||||
WalletTokensListState.Content(
|
||||
items = when (value) {
|
||||
is TokenList.GroupedByNetwork -> value.mapToMultiCurrencyItems()
|
||||
is TokenList.Ungrouped -> value.mapToMultiCurrencyItems()
|
||||
is TokenList.NotInitialized -> getLoadingMultiCurrencyTokens()
|
||||
},
|
||||
onOrganizeTokensClick = if (value.totalFiatBalance is TokenList.FiatBalance.Loaded) {
|
||||
clickIntents::onOrganizeTokensClick
|
||||
} else {
|
||||
null
|
||||
},
|
||||
return when (value) {
|
||||
is TokenList.NotInitialized -> WalletTokensListState.Loading()
|
||||
is TokenList.GroupedByNetwork -> WalletTokensListState.Content(
|
||||
items = value.mapToMultiCurrencyItems(),
|
||||
organizeTokensButton = value.mapToOrganizeTokensButtonState(),
|
||||
)
|
||||
is TokenList.Ungrouped -> WalletTokensListState.Content(
|
||||
items = value.mapToMultiCurrencyItems(),
|
||||
organizeTokensButton = value.mapToOrganizeTokensButtonState(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -64,6 +53,20 @@ internal class TokenListToContentItemsConverter(
|
|||
}
|
||||
}
|
||||
|
||||
private fun TokenList.GroupedByNetwork.mapToOrganizeTokensButtonState(): OrganizeTokensButtonState {
|
||||
return getOrganizeTokensButtonState(
|
||||
isLoading = totalFiatBalance is TokenList.FiatBalance.Loading,
|
||||
currenciesSize = groups.flatMap(NetworkGroup::currencies).size,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenList.Ungrouped.mapToOrganizeTokensButtonState(): OrganizeTokensButtonState {
|
||||
return getOrganizeTokensButtonState(
|
||||
isLoading = totalFiatBalance is TokenList.FiatBalance.Loading,
|
||||
currenciesSize = currencies.size,
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<TokensListItemState>.addGroup(group: NetworkGroup): List<TokensListItemState> {
|
||||
this.add(TokensListItemState.NetworkGroupTitle(TextReference.Str(group.network.name)))
|
||||
|
||||
|
|
@ -81,4 +84,15 @@ internal class TokenListToContentItemsConverter(
|
|||
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getOrganizeTokensButtonState(isLoading: Boolean, currenciesSize: Int): OrganizeTokensButtonState {
|
||||
return if (currenciesSize > 1) {
|
||||
OrganizeTokensButtonState.Visible(
|
||||
isEnabled = !isLoading,
|
||||
onClick = clickIntents::onOrganizeTokensClick,
|
||||
)
|
||||
} else {
|
||||
OrganizeTokensButtonState.Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,9 +6,9 @@ import com.tangem.domain.common.CardTypesResolver
|
|||
import com.tangem.domain.tokens.model.TokenList
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter.TokensListModel
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
|
@ -21,7 +21,7 @@ internal class TokenListToWalletStateConverter(
|
|||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val isWalletContentHidden: Boolean,
|
||||
clickIntents: WalletClickIntents,
|
||||
) : Converter<TokensListModel, WalletMultiCurrencyState.Content> {
|
||||
) : Converter<TokenList, WalletState> {
|
||||
|
||||
private val tokenListToContentConverter = TokenListToContentItemsConverter(
|
||||
isWalletContentHidden = isWalletContentHidden,
|
||||
|
|
@ -29,17 +29,20 @@ internal class TokenListToWalletStateConverter(
|
|||
clickIntents = clickIntents,
|
||||
)
|
||||
|
||||
override fun convert(value: TokensListModel): WalletMultiCurrencyState.Content {
|
||||
val state = requireNotNull(currentStateProvider() as? WalletMultiCurrencyState.Content)
|
||||
return state.copy(
|
||||
walletsListConfig = state.updateSelectedWallet(fiatBalance = value.tokenList.totalFiatBalance),
|
||||
pullToRefreshConfig = if (value.isRefreshing) {
|
||||
state.pullToRefreshConfig.copy(isRefreshing = getRefreshingStatus(tokenList = value.tokenList))
|
||||
} else {
|
||||
state.pullToRefreshConfig
|
||||
},
|
||||
tokensListState = tokenListToContentConverter.convert(value = value.tokenList),
|
||||
)
|
||||
override fun convert(value: TokenList): WalletState {
|
||||
return when (val state = currentStateProvider()) {
|
||||
is WalletMultiCurrencyState.Content -> {
|
||||
state.copy(
|
||||
walletsListConfig = state.updateSelectedWallet(fiatBalance = value.totalFiatBalance),
|
||||
tokensListState = tokenListToContentConverter.convert(value = value),
|
||||
)
|
||||
}
|
||||
is WalletMultiCurrencyState.Locked,
|
||||
is WalletSingleCurrencyState.Content,
|
||||
is WalletSingleCurrencyState.Locked,
|
||||
is WalletState.Initial,
|
||||
-> state
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletMultiCurrencyState.updateSelectedWallet(fiatBalance: TokenList.FiatBalance): WalletsListConfig {
|
||||
|
|
@ -58,10 +61,4 @@ internal class TokenListToWalletStateConverter(
|
|||
.set(index = selectedWalletIndex, element = converter.convert(fiatBalance)),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getRefreshingStatus(tokenList: TokenList): Boolean {
|
||||
return tokenList.totalFiatBalance is TokenList.FiatBalance.Loading
|
||||
}
|
||||
|
||||
data class TokensListModel(val tokenList: TokenList, val isRefreshing: Boolean)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.viewmodels
|
||||
|
||||
import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
|
|
@ -11,6 +12,10 @@ internal interface WalletClickIntents : TxHistoryClickIntents {
|
|||
|
||||
fun onScanCardClick()
|
||||
|
||||
fun onScanCardNotificationClick()
|
||||
|
||||
fun onScanToUnlockWalletClick()
|
||||
|
||||
fun onDetailsClick()
|
||||
|
||||
fun onBackupCardClick()
|
||||
|
|
@ -39,7 +44,7 @@ internal interface WalletClickIntents : TxHistoryClickIntents {
|
|||
|
||||
fun onTokenItemClick(currency: CryptoCurrency)
|
||||
|
||||
fun onTokenItemLongClick(currency: CryptoCurrency)
|
||||
fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
|
||||
|
||||
fun onDismissActionsBottomSheet()
|
||||
|
||||
|
|
@ -47,7 +52,9 @@ internal interface WalletClickIntents : TxHistoryClickIntents {
|
|||
|
||||
fun onDeleteClick(userWalletId: UserWalletId)
|
||||
|
||||
fun onSendClick()
|
||||
fun onSingleCurrencySendClick(cryptoCurrencyStatus: CryptoCurrencyStatus? = null)
|
||||
|
||||
fun onMultiCurrencySendClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
|
||||
|
||||
fun onReceiveClick()
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ internal class WalletNotificationsListFactory(
|
|||
}
|
||||
|
||||
if (tokenList != null && tokenList.hasMissedDerivations()) {
|
||||
add(element = WalletNotification.ScanCard(onClick = clickIntents::onScanCardClick))
|
||||
add(element = WalletNotification.ScanCard(onClick = clickIntents::onScanCardNotificationClick))
|
||||
}
|
||||
|
||||
if (isUserAlreadyRateAppCallback()) {
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
|||
*/
|
||||
internal object WalletStateCache {
|
||||
|
||||
private val states = mutableMapOf<UserWalletId, WalletState>()
|
||||
private val states = mutableMapOf<UserWalletId, WalletState.ContentState>()
|
||||
|
||||
/** Get state by [userWalletId] */
|
||||
fun getState(userWalletId: UserWalletId): WalletState? = states[userWalletId]
|
||||
fun getState(userWalletId: UserWalletId): WalletState.ContentState? = states[userWalletId]
|
||||
|
||||
/** Add or update [state] by [userWalletId] */
|
||||
fun update(userWalletId: UserWalletId, state: WalletState) {
|
||||
fun update(userWalletId: UserWalletId, state: WalletState.ContentState) {
|
||||
states[userWalletId] = state
|
||||
}
|
||||
}
|
||||
|
|
@ -21,9 +21,7 @@ import com.tangem.domain.redux.ReduxStateHolder
|
|||
import com.tangem.domain.settings.CanUseBiometryUseCase
|
||||
import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase
|
||||
import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
|
||||
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.tokens.GetTokenListUseCase
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.TokenList
|
||||
|
|
@ -49,6 +47,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -65,7 +64,7 @@ import kotlin.properties.Delegates
|
|||
internal class WalletViewModel @Inject constructor(
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
private val selectWalletUseCase: SelectWalletUseCase,
|
||||
private val updateWalletUseCase: UpdateWalletUseCase,
|
||||
private val deleteWalletUseCase: DeleteWalletUseCase,
|
||||
|
|
@ -73,7 +72,10 @@ internal class WalletViewModel @Inject constructor(
|
|||
private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase,
|
||||
private val getAccessCodeSavingStatusUseCase: GetAccessCodeSavingStatusUseCase,
|
||||
private val getTokenListUseCase: GetTokenListUseCase,
|
||||
private val getPrimaryCurrencyUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
|
||||
private val fetchTokenListUseCase: FetchTokenListUseCase,
|
||||
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
|
||||
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
|
||||
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
|
||||
private val getCardWasScannedUseCase: GetCardWasScannedUseCase,
|
||||
private val isUserAlreadyRateAppUseCase: IsUserAlreadyRateAppUseCase,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
|
|
@ -121,12 +123,18 @@ internal class WalletViewModel @Inject constructor(
|
|||
var uiState: WalletState by uiStateHolder(initialState = stateFactory.getInitialState())
|
||||
|
||||
private var wallets: List<UserWallet> by Delegates.notNull()
|
||||
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
|
||||
private var singleWalletCryptoCurrencyStatus: CryptoCurrencyStatus? = null
|
||||
|
||||
private val tokensJobHolder = JobHolder()
|
||||
private val marketPriceJobHolder = JobHolder()
|
||||
private val buttonsJobHolder = JobHolder()
|
||||
private val notificationsJobHolder = JobHolder()
|
||||
private val refreshContentJobHolder = JobHolder()
|
||||
|
||||
private val walletsUpdateActionResolver = WalletsUpdateActionResolver(
|
||||
currentStateProvider = Provider { uiState },
|
||||
getSelectedWalletUseCase = getSelectedWalletUseCase,
|
||||
)
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
|
|
@ -146,154 +154,51 @@ internal class WalletViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun updateWallets(sourceList: List<UserWallet>) {
|
||||
if (sourceList.isEmpty()) return
|
||||
|
||||
wallets = sourceList
|
||||
|
||||
val currentState = uiState
|
||||
val selectedWalletIndex = if (currentState is WalletLockedState) {
|
||||
currentState.getSelectedWalletIndex()
|
||||
if (sourceList.isEmpty()) return
|
||||
|
||||
when (val action = walletsUpdateActionResolver.resolve(sourceList)) {
|
||||
is WalletsUpdateActionResolver.Action.InitialWallets -> {
|
||||
loadAndUpdateState(index = action.selectedWalletIndex)
|
||||
}
|
||||
is WalletsUpdateActionResolver.Action.UpdateWalletName -> {
|
||||
uiState = stateFactory.getStateWithUpdatedWalletName(name = action.name)
|
||||
}
|
||||
is WalletsUpdateActionResolver.Action.UnlockWallet -> {
|
||||
uiState = stateFactory.getUnlockedState(action)
|
||||
|
||||
getContentItemsUpdates(index = action.selectedWalletIndex)
|
||||
}
|
||||
is WalletsUpdateActionResolver.Action.DeleteWallet -> {
|
||||
deleteWalletAndUpdateState(action = action)
|
||||
}
|
||||
is WalletsUpdateActionResolver.Action.AddWallet -> {
|
||||
loadAndUpdateState(index = action.selectedWalletIndex)
|
||||
}
|
||||
is WalletsUpdateActionResolver.Action.Unknown -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadAndUpdateState(index: Int) {
|
||||
uiState = stateFactory.getSkeletonState(wallets = wallets, selectedWalletIndex = index)
|
||||
|
||||
getContentItemsUpdates(index = index)
|
||||
}
|
||||
|
||||
private fun deleteWalletAndUpdateState(action: WalletsUpdateActionResolver.Action.DeleteWallet) {
|
||||
val cacheState = WalletStateCache.getState(userWalletId = action.selectedWalletId)
|
||||
if (cacheState != null) {
|
||||
uiState = stateFactory.getStateWithoutDeletedWallet(cacheState, action)
|
||||
|
||||
if (cacheState.isLoadingState()) {
|
||||
getContentItemsUpdates(action.selectedWalletIndex)
|
||||
}
|
||||
} else {
|
||||
val selectedWallet = getSelectedWalletUseCase().fold(
|
||||
ifLeft = { error("Selected wallet is null") },
|
||||
ifRight = { it },
|
||||
)
|
||||
sourceList.indexOfFirst { it.walletId == selectedWallet.walletId }
|
||||
}
|
||||
|
||||
uiState = stateFactory.getSkeletonState(wallets = sourceList, selectedWalletIndex = selectedWalletIndex)
|
||||
|
||||
updateContentItems(index = selectedWalletIndex)
|
||||
}
|
||||
|
||||
private fun updateContentItems(index: Int, isRefreshing: Boolean = false) {
|
||||
val cardTypeResolver = getCardTypeResolver(index)
|
||||
when {
|
||||
getWallet(index).isLocked -> uiState = stateFactory.getLockedState()
|
||||
cardTypeResolver.isMultiwalletAllowed() -> updateMultiCurrencyContent(index, isRefreshing)
|
||||
!cardTypeResolver.isMultiwalletAllowed() -> updateSingleCurrencyContent(index, isRefreshing)
|
||||
loadAndUpdateState(index = action.selectedWalletIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMultiCurrencyContent(index: Int, isRefreshing: Boolean = false) {
|
||||
val state = requireNotNull(uiState as? WalletMultiCurrencyState) {
|
||||
"Impossible to update tokens list if state isn't WalletMultiCurrencyState"
|
||||
}
|
||||
|
||||
getTokenListUseCase(userWalletId = state.walletsListConfig.wallets[index].id)
|
||||
.distinctUntilChanged()
|
||||
.onEach { tokenListEither ->
|
||||
uiState = stateFactory.getStateByTokensList(
|
||||
tokenListEither = tokenListEither,
|
||||
isRefreshing = isRefreshing,
|
||||
)
|
||||
|
||||
updateNotifications(
|
||||
index = index,
|
||||
tokenList = tokenListEither.fold(ifLeft = { null }, ifRight = { it }),
|
||||
)
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(tokensJobHolder)
|
||||
}
|
||||
|
||||
private fun updateSingleCurrencyContent(index: Int, isRefreshing: Boolean) {
|
||||
val wallet = getWallet(index)
|
||||
val blockchain = getCardTypeResolver(index).getBlockchain()
|
||||
updateButtons(userWalletId = wallet.walletId, currencyId = blockchain.id)
|
||||
updateTxHistory(
|
||||
blockchain = blockchain,
|
||||
derivationStyle = wallet.scanResponse.derivationStyleProvider.getDerivationStyle(),
|
||||
)
|
||||
updateMarketPrice(userWalletId = wallet.walletId, isRefreshing = isRefreshing)
|
||||
updateNotifications(index)
|
||||
}
|
||||
|
||||
private fun updateTxHistory(blockchain: Blockchain, derivationStyle: DerivationStyle?) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val derivationPath = blockchain.derivationPath(style = derivationStyle)?.rawPath
|
||||
|
||||
val txHistoryItemsCountEither = txHistoryItemsCountUseCase(
|
||||
networkId = Network.ID(blockchain.id),
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
|
||||
uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither)
|
||||
|
||||
txHistoryItemsCountEither.onRight {
|
||||
uiState = stateFactory.getLoadedTxHistoryState(
|
||||
txHistoryEither = txHistoryItemsUseCase(
|
||||
networkId = Network.ID(blockchain.id),
|
||||
derivationPath = derivationPath,
|
||||
).map {
|
||||
it.cachedIn(viewModelScope)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It also update wallet balance
|
||||
private fun updateMarketPrice(userWalletId: UserWalletId, isRefreshing: Boolean) {
|
||||
getPrimaryCurrencyUseCase(userWalletId = userWalletId)
|
||||
.distinctUntilChanged()
|
||||
.onEach { either ->
|
||||
uiState = stateFactory.getSingleCurrencyLoadedBalanceState(
|
||||
cryptoCurrencyEither = either,
|
||||
isRefreshing = isRefreshing,
|
||||
)
|
||||
|
||||
either.onRight { status -> cryptoCurrencyStatus = status }
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(marketPriceJobHolder)
|
||||
}
|
||||
|
||||
private fun updateButtons(userWalletId: UserWalletId, currencyId: String) {
|
||||
getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, tokenId = currencyId)
|
||||
.distinctUntilChanged()
|
||||
.onEach { uiState = stateFactory.getSingleCurrencyManageButtonsState(actions = it.states) }
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(buttonsJobHolder)
|
||||
}
|
||||
|
||||
private fun updateNotifications(index: Int, tokenList: TokenList? = null) {
|
||||
notificationsListFactory.create(
|
||||
cardTypesResolver = getCardTypeResolver(index = index),
|
||||
tokenList = tokenList,
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
.onEach { uiState = stateFactory.getStateByNotifications(notifications = it) }
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(notificationsJobHolder)
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
saveSelectedWallet()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveSelectedWallet() {
|
||||
val state = uiState
|
||||
if (state is WalletState.ContentState) {
|
||||
selectWalletUseCase(getWallet(index = state.walletsListConfig.selectedWalletIndex).walletId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getWallet(index: Int): UserWallet {
|
||||
return requireNotNull(
|
||||
value = wallets.getOrNull(index),
|
||||
lazyMessage = { "WalletsList doesn't contain element with index = $index" },
|
||||
)
|
||||
}
|
||||
|
||||
private fun getCardTypeResolver(index: Int): CardTypesResolver = getWallet(index).scanResponse.cardTypesResolver
|
||||
|
||||
override fun onBackClick() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
router.popBackStack(screen = if (shouldSaveUserWalletsUseCase()) AppScreen.Welcome else AppScreen.Home)
|
||||
|
|
@ -301,23 +206,56 @@ internal class WalletViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onScanCardClick() {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
scanCardProcessor.scan()
|
||||
.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 = userWallet, canOverride = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScanCardNotificationClick() {
|
||||
scanToUpdateSelectedWallet(
|
||||
onSuccessSave = {
|
||||
// Reload currencies with missed derivation
|
||||
fetchTokenListUseCase(userWalletId = it.walletId)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun onScanToUnlockWalletClick() {
|
||||
scanToUpdateSelectedWallet()
|
||||
}
|
||||
|
||||
private fun scanToUpdateSelectedWallet(onSuccessSave: suspend (UserWallet) -> Unit = {}) {
|
||||
val state = uiState as? WalletState.ContentState ?: return
|
||||
|
||||
val prevRequestPolicyStatus = getBiometricsStatusUseCase()
|
||||
|
||||
// Update access the code policy according access code saving status
|
||||
setAccessCodeRequestPolicyUseCase(isBiometricsRequestPolicy = getAccessCodeSavingStatusUseCase())
|
||||
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true)
|
||||
scanCardProcessor.scan(
|
||||
cardId = getWallet(state.walletsListConfig.selectedWalletIndex).cardId,
|
||||
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)
|
||||
saveWalletUseCase(userWallet = userWallet, canOverride = true)
|
||||
.onLeft {
|
||||
// Rollback policy if card saving was failed
|
||||
setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus)
|
||||
}
|
||||
.onRight { onSuccessSave(userWallet) }
|
||||
} else {
|
||||
// Rollback policy if card saving was failed
|
||||
setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus)
|
||||
|
|
@ -365,76 +303,61 @@ internal class WalletViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onWalletChange(index: Int) {
|
||||
val state = requireNotNull(uiState as? WalletState.ContentState) {
|
||||
"Impossible to change wallet if state isn't WalletState.ContentState"
|
||||
}
|
||||
|
||||
val state = uiState as? WalletState.ContentState ?: return
|
||||
if (state.walletsListConfig.selectedWalletIndex == index) return
|
||||
|
||||
/*
|
||||
* When wallet is changed it's necessary to stop the last jobs.
|
||||
* If jobs aren't stopped and wallet is changed then it will update state for the prev wallet.
|
||||
*/
|
||||
tokensJobHolder.update(job = null)
|
||||
marketPriceJobHolder.update(job = null)
|
||||
buttonsJobHolder.update(job = null)
|
||||
notificationsJobHolder.update(job = null)
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
selectWalletUseCase(getWallet(index = index).walletId)
|
||||
}
|
||||
|
||||
val cacheState = WalletStateCache.getState(userWalletId = state.walletsListConfig.wallets[index].id)
|
||||
if (cacheState != null) {
|
||||
uiState = if (cacheState is WalletState.ContentState) {
|
||||
cacheState.copySealed(
|
||||
walletsListConfig = state.walletsListConfig.copy(selectedWalletIndex = index),
|
||||
pullToRefreshConfig = state.pullToRefreshConfig.copy(isRefreshing = false),
|
||||
)
|
||||
} else {
|
||||
cacheState
|
||||
}
|
||||
if (cacheState != null && cacheState !is WalletLockedState) {
|
||||
uiState = cacheState.copySealed(
|
||||
walletsListConfig = state.walletsListConfig.copy(
|
||||
selectedWalletIndex = index,
|
||||
wallets = state.walletsListConfig.wallets
|
||||
.mapIndexed { mapIndex, currentWallet ->
|
||||
val cacheWallet = cacheState.walletsListConfig.wallets.getOrNull(mapIndex)
|
||||
|
||||
if (cacheState.isLoadingState()) updateContentItems(index)
|
||||
if (currentWallet is WalletCardState.Loading && cacheWallet != null &&
|
||||
cacheWallet.isLoaded()
|
||||
) {
|
||||
cacheWallet
|
||||
} else {
|
||||
currentWallet
|
||||
}
|
||||
}
|
||||
.toImmutableList(),
|
||||
),
|
||||
pullToRefreshConfig = cacheState.pullToRefreshConfig.copy(isRefreshing = false),
|
||||
)
|
||||
|
||||
if (cacheState.isLoadingState()) {
|
||||
getContentItemsUpdates(index)
|
||||
}
|
||||
} else {
|
||||
uiState = stateFactory.getSkeletonState(wallets = wallets, selectedWalletIndex = index)
|
||||
updateContentItems(index = index)
|
||||
getContentItemsUpdates(index = index)
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletState.isLoadingState(): Boolean {
|
||||
// Check the base components
|
||||
if (this is WalletState.ContentState) {
|
||||
walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] is WalletCardState.Loading ||
|
||||
notifications.isEmpty()
|
||||
}
|
||||
|
||||
// Check the special components
|
||||
return when (this) {
|
||||
is WalletMultiCurrencyState -> {
|
||||
val hasLoadingTokens = tokensListState is WalletTokensListState.ContentState &&
|
||||
(tokensListState as WalletTokensListState.ContentState).items
|
||||
.filterIsInstance<WalletTokensListState.TokensListItemState.Token>()
|
||||
.any { it.state is TokenItemState.Loading }
|
||||
|
||||
tokensListState is WalletTokensListState.Loading || hasLoadingTokens
|
||||
}
|
||||
is WalletSingleCurrencyState -> {
|
||||
this is WalletSingleCurrencyState.Content && marketPriceBlockState is MarketPriceBlockState.Loading
|
||||
}
|
||||
is WalletState.Initial -> false
|
||||
}
|
||||
private fun WalletCardState.isLoaded(): Boolean {
|
||||
return this !is WalletCardState.Loading && this !is WalletCardState.LockedContent
|
||||
}
|
||||
|
||||
override fun onRefreshSwipe() {
|
||||
if (uiState is WalletState.Initial || uiState is WalletLockedState) return
|
||||
val selectedWalletIndex = (uiState as? WalletState.ContentState)
|
||||
?.walletsListConfig
|
||||
?.selectedWalletIndex
|
||||
?: return
|
||||
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
uiState = stateFactory.getStateAfterContentRefreshing()
|
||||
|
||||
// TODO: [REDACTED_JIRA]
|
||||
delay(timeMillis = 500)
|
||||
|
||||
updateContentItems(
|
||||
index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex,
|
||||
isRefreshing = true,
|
||||
)
|
||||
when (uiState) {
|
||||
is WalletMultiCurrencyState.Content -> refreshMultiCurrencyContent(selectedWalletIndex)
|
||||
is WalletSingleCurrencyState.Content -> refreshSingleCurrencyContent(selectedWalletIndex)
|
||||
is WalletState.Initial,
|
||||
is WalletMultiCurrencyState.Locked,
|
||||
is WalletSingleCurrencyState.Locked,
|
||||
-> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -448,7 +371,7 @@ internal class WalletViewModel @Inject constructor(
|
|||
|
||||
override fun onBuyClick() {
|
||||
val state = uiState as? WalletState.ContentState ?: return
|
||||
val status = cryptoCurrencyStatus ?: return
|
||||
val status = singleWalletCryptoCurrencyStatus ?: return
|
||||
val wallet = getWallet(index = state.walletsListConfig.selectedWalletIndex)
|
||||
|
||||
reduxStateHolder.dispatch(
|
||||
|
|
@ -460,8 +383,48 @@ internal class WalletViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override fun onSendClick() {
|
||||
reduxStateHolder.dispatch(TradeCryptoAction.New.Send)
|
||||
override fun onSingleCurrencySendClick(cryptoCurrencyStatus: CryptoCurrencyStatus?) {
|
||||
val state = uiState as? WalletState.ContentState ?: return
|
||||
|
||||
val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex)
|
||||
val coinStatus = if (userWallet.isMultiCurrency) cryptoCurrencyStatus else singleWalletCryptoCurrencyStatus
|
||||
|
||||
reduxStateHolder.dispatch(
|
||||
action = TradeCryptoAction.New.SendCoin(
|
||||
userWallet = userWallet,
|
||||
coinStatus = coinStatus ?: return,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onMultiCurrencySendClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {
|
||||
if (cryptoCurrencyStatus.currency is CryptoCurrency.Coin) {
|
||||
onSingleCurrencySendClick(cryptoCurrencyStatus = cryptoCurrencyStatus)
|
||||
return
|
||||
}
|
||||
|
||||
val state = uiState as? WalletState.ContentState ?: return
|
||||
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex)
|
||||
|
||||
getNetworkCoinStatusUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
networkId = cryptoCurrencyStatus.currency.network.id,
|
||||
)
|
||||
.take(count = 1)
|
||||
.collectLatest {
|
||||
it.onRight { coinStatus ->
|
||||
reduxStateHolder.dispatch(
|
||||
action = TradeCryptoAction.New.SendToken(
|
||||
userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex),
|
||||
tokenStatus = cryptoCurrencyStatus,
|
||||
coinFiatRate = coinStatus.value.fiatRate,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceiveClick() {
|
||||
|
|
@ -469,7 +432,7 @@ internal class WalletViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onSellClick() {
|
||||
val status = cryptoCurrencyStatus ?: return
|
||||
val status = singleWalletCryptoCurrencyStatus ?: return
|
||||
|
||||
reduxStateHolder.dispatch(
|
||||
TradeCryptoAction.New.Sell(
|
||||
|
|
@ -480,15 +443,17 @@ internal class WalletViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onManageTokensClick() {
|
||||
reduxStateHolder.dispatch(action = TokensAction.SetArgs.ManageAccess)
|
||||
router.openManageTokensScreen()
|
||||
}
|
||||
|
||||
override fun onReloadClick() {
|
||||
uiState = stateFactory.getStateAfterContentRefreshing()
|
||||
updateSingleCurrencyContent(
|
||||
index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex,
|
||||
isRefreshing = true,
|
||||
)
|
||||
val selectedWalletIndex = (uiState as? WalletSingleCurrencyState)
|
||||
?.walletsListConfig
|
||||
?.selectedWalletIndex
|
||||
?: return
|
||||
|
||||
refreshSingleCurrencyContent(selectedWalletIndex)
|
||||
}
|
||||
|
||||
override fun onExploreClick() {
|
||||
|
|
@ -530,10 +495,8 @@ internal class WalletViewModel @Inject constructor(
|
|||
router.openTokenDetails(currency = currency)
|
||||
}
|
||||
|
||||
override fun onTokenItemLongClick(currency: CryptoCurrency) {
|
||||
uiState = stateFactory.getStateWithTokenActionBottomSheet(
|
||||
tokenId = currency.id.value,
|
||||
)
|
||||
override fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {
|
||||
uiState = stateFactory.getStateWithTokenActionBottomSheet(cryptoCurrencyStatus)
|
||||
}
|
||||
|
||||
override fun onRenameClick(userWalletId: UserWalletId, name: String) {
|
||||
|
|
@ -543,26 +506,15 @@ internal class WalletViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onDeleteClick(userWalletId: UserWalletId) {
|
||||
val state = uiState as? WalletState.ContentState ?: return
|
||||
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val either = deleteWalletUseCase(userWalletId)
|
||||
|
||||
val state = requireNotNull(uiState as? WalletState.ContentState)
|
||||
if (state.walletsListConfig.wallets.size <= 1 && either.isRight()) onBackClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
|
||||
return getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = AppCurrency.Default,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDismissBottomSheet() {
|
||||
uiState = stateFactory.getStateWithClosedBottomSheet()
|
||||
}
|
||||
|
|
@ -576,4 +528,189 @@ internal class WalletViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContentItemsUpdates(index: Int) {
|
||||
/*
|
||||
* When wallet is changed it's necessary to stop the last jobs.
|
||||
* If jobs aren't stopped and wallet is changed then it will update state for the prev wallet.
|
||||
*/
|
||||
tokensJobHolder.update(job = null)
|
||||
marketPriceJobHolder.update(job = null)
|
||||
buttonsJobHolder.update(job = null)
|
||||
notificationsJobHolder.update(job = null)
|
||||
refreshContentJobHolder.update(job = null)
|
||||
|
||||
val wallet = getWallet(index)
|
||||
|
||||
when {
|
||||
wallet.isLocked -> {
|
||||
uiState = stateFactory.getLockedState()
|
||||
}
|
||||
wallet.isMultiCurrency -> getMultiCurrencyContent(index)
|
||||
!wallet.isMultiCurrency -> getSingleCurrencyContent(index)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMultiCurrencyContent(walletIndex: Int) {
|
||||
val state = requireNotNull(uiState as? WalletMultiCurrencyState) {
|
||||
"Impossible to get a token list updates if state isn't WalletMultiCurrencyState"
|
||||
}
|
||||
|
||||
getTokenListUseCase(userWalletId = state.walletsListConfig.wallets[walletIndex].id)
|
||||
.distinctUntilChanged()
|
||||
.onEach { maybeTokenList ->
|
||||
uiState = stateFactory.getStateByTokensList(maybeTokenList)
|
||||
|
||||
updateNotifications(
|
||||
index = walletIndex,
|
||||
tokenList = maybeTokenList.fold(ifLeft = { null }, ifRight = { it }),
|
||||
)
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(tokensJobHolder)
|
||||
}
|
||||
|
||||
private fun getSingleCurrencyContent(index: Int) {
|
||||
val wallet = getWallet(index)
|
||||
val blockchain = getCardTypeResolver(index).getBlockchain()
|
||||
updateTxHistory(
|
||||
blockchain = blockchain,
|
||||
derivationStyle = wallet.scanResponse.derivationStyleProvider.getDerivationStyle(),
|
||||
)
|
||||
updateMarketPrice(userWalletId = wallet.walletId)
|
||||
updateNotifications(index)
|
||||
}
|
||||
|
||||
private fun updateTxHistory(blockchain: Blockchain, derivationStyle: DerivationStyle?) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val derivationPath = blockchain.derivationPath(style = derivationStyle)?.rawPath
|
||||
|
||||
val txHistoryItemsCountEither = txHistoryItemsCountUseCase(
|
||||
networkId = Network.ID(blockchain.id),
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
|
||||
uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither)
|
||||
|
||||
txHistoryItemsCountEither.onRight {
|
||||
uiState = stateFactory.getLoadedTxHistoryState(
|
||||
txHistoryEither = txHistoryItemsUseCase(
|
||||
networkId = Network.ID(blockchain.id),
|
||||
derivationPath = derivationPath,
|
||||
).map {
|
||||
it.cachedIn(viewModelScope)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It also update wallet balance
|
||||
private fun updateMarketPrice(userWalletId: UserWalletId) {
|
||||
getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId)
|
||||
.distinctUntilChanged()
|
||||
.onEach { maybeCryptoCurrencyStatus ->
|
||||
uiState = stateFactory.getSingleCurrencyLoadedBalanceState(maybeCryptoCurrencyStatus)
|
||||
|
||||
maybeCryptoCurrencyStatus.onRight { status ->
|
||||
singleWalletCryptoCurrencyStatus = status
|
||||
updateButtons(userWalletId = userWalletId, currency = status.currency)
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(marketPriceJobHolder)
|
||||
}
|
||||
|
||||
private fun updateButtons(userWalletId: UserWalletId, currency: CryptoCurrency) {
|
||||
getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, cryptoCurrency = currency)
|
||||
.distinctUntilChanged()
|
||||
.onEach { uiState = stateFactory.getSingleCurrencyManageButtonsState(actions = it.states) }
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(buttonsJobHolder)
|
||||
}
|
||||
|
||||
private fun updateNotifications(index: Int, tokenList: TokenList? = null) {
|
||||
notificationsListFactory.create(
|
||||
cardTypesResolver = getCardTypeResolver(index = index),
|
||||
tokenList = tokenList,
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
.onEach { uiState = stateFactory.getStateByNotifications(notifications = it) }
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(notificationsJobHolder)
|
||||
}
|
||||
|
||||
private fun refreshMultiCurrencyContent(walletIndex: Int) {
|
||||
uiState = stateFactory.getRefreshingState()
|
||||
val wallet = getWallet(walletIndex)
|
||||
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val result = fetchTokenListUseCase(wallet.walletId, refresh = true)
|
||||
|
||||
uiState = stateFactory.getRefreshedState()
|
||||
uiState = result.fold(stateFactory::getStateByTokenListError) { uiState }
|
||||
}.saveIn(refreshContentJobHolder)
|
||||
}
|
||||
|
||||
private fun refreshSingleCurrencyContent(walletIndex: Int) {
|
||||
uiState = stateFactory.getRefreshingState()
|
||||
val wallet = getWallet(walletIndex)
|
||||
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val result = fetchCurrencyStatusUseCase(wallet.walletId, refresh = true)
|
||||
|
||||
uiState = stateFactory.getRefreshedState()
|
||||
uiState = result.fold(stateFactory::getStateByCurrencyStatusError) { uiState }
|
||||
}.saveIn(refreshContentJobHolder)
|
||||
}
|
||||
|
||||
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
|
||||
return getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = AppCurrency.Default,
|
||||
)
|
||||
}
|
||||
|
||||
private fun WalletState.isLoadingState(): Boolean {
|
||||
// Check the base components
|
||||
if (this is WalletState.ContentState &&
|
||||
walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] is WalletCardState.Loading
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check the special components
|
||||
return when (this) {
|
||||
is WalletMultiCurrencyState -> {
|
||||
val hasLoadingTokens = tokensListState is WalletTokensListState.ContentState &&
|
||||
(tokensListState as WalletTokensListState.ContentState).items
|
||||
.filterIsInstance<WalletTokensListState.TokensListItemState.Token>()
|
||||
.any { it.state is TokenItemState.Loading }
|
||||
|
||||
tokensListState is WalletTokensListState.Loading || hasLoadingTokens
|
||||
}
|
||||
is WalletSingleCurrencyState -> {
|
||||
this is WalletSingleCurrencyState.Content && marketPriceBlockState is MarketPriceBlockState.Loading
|
||||
}
|
||||
is WalletState.Initial -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getWallet(index: Int): UserWallet {
|
||||
return requireNotNull(
|
||||
value = wallets.getOrNull(index),
|
||||
lazyMessage = { "WalletsList doesn't contain element with index = $index" },
|
||||
)
|
||||
}
|
||||
|
||||
private fun getCardTypeResolver(index: Int): CardTypesResolver = getWallet(index).scanResponse.cardTypesResolver
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.viewmodels
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletLockedState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
|
||||
|
||||
/**
|
||||
* Resolver that determines which update action will be performed
|
||||
*
|
||||
* @property currentStateProvider current state provider
|
||||
* @property getSelectedWalletUseCase use case that returns selected wallet
|
||||
*/
|
||||
internal class WalletsUpdateActionResolver(
|
||||
private val currentStateProvider: Provider<WalletState>,
|
||||
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
) {
|
||||
|
||||
fun resolve(wallets: List<UserWallet>): Action {
|
||||
val selectedWallet = wallets.getSelectedWallet()
|
||||
|
||||
return when (val state = currentStateProvider()) {
|
||||
is WalletState.Initial -> {
|
||||
Action.InitialWallets(
|
||||
selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId),
|
||||
)
|
||||
}
|
||||
is WalletState.ContentState -> {
|
||||
getActionToUpdateContent(state = state, wallets = wallets, selectedWallet = selectedWallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<UserWallet>.getSelectedWallet(): UserWallet {
|
||||
val hasUnlockedWallet = any { !it.isLocked }
|
||||
return if (hasUnlockedWallet) {
|
||||
val selectedWalletId = getSelectedWalletUseCase().fold(ifLeft = ::error, ifRight = UserWallet::walletId)
|
||||
|
||||
firstOrNull { it.walletId == selectedWalletId }
|
||||
?: error("Wallets don't contain a wallet with id: $selectedWalletId")
|
||||
} else {
|
||||
lastOrNull() ?: error("Wallets is empty")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getActionToUpdateContent(
|
||||
state: WalletState.ContentState,
|
||||
wallets: List<UserWallet>,
|
||||
selectedWallet: UserWallet,
|
||||
): Action {
|
||||
return if (isWalletsCountChanged(state, wallets)) {
|
||||
getActionToChangeWallets(state = state, wallets = wallets, selectedWallet = selectedWallet)
|
||||
} else {
|
||||
getActionToUpdateCurrentWallet(state = state, wallets = wallets, selectedWallet = selectedWallet)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isWalletsCountChanged(state: WalletState.ContentState, wallets: List<UserWallet>): Boolean {
|
||||
val prevWalletsSize = state.walletsListConfig.wallets.size
|
||||
val walletsSize = wallets.size
|
||||
|
||||
return prevWalletsSize != walletsSize
|
||||
}
|
||||
|
||||
private fun getActionToChangeWallets(
|
||||
state: WalletState.ContentState,
|
||||
wallets: List<UserWallet>,
|
||||
selectedWallet: UserWallet,
|
||||
): Action {
|
||||
val prevWalletsSize = state.walletsListConfig.wallets.size
|
||||
|
||||
return when {
|
||||
prevWalletsSize > wallets.size -> {
|
||||
Action.DeleteWallet(
|
||||
selectedWalletId = selectedWallet.walletId,
|
||||
selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId),
|
||||
deletedWalletId = state.walletsListConfig.wallets.getDeletedWalletId(wallets),
|
||||
)
|
||||
}
|
||||
prevWalletsSize < wallets.size -> {
|
||||
Action.AddWallet(
|
||||
selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId),
|
||||
)
|
||||
}
|
||||
else -> Action.Unknown
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<WalletCardState>.getDeletedWalletId(wallets: List<UserWallet>): UserWalletId {
|
||||
return this
|
||||
.map(WalletCardState::id)
|
||||
.firstOrNull { !wallets.map(UserWallet::walletId).contains(it) }
|
||||
?: error("Deleted wallet id is not found. Wallets contains all previous wallets ids")
|
||||
}
|
||||
|
||||
private fun getActionToUpdateCurrentWallet(
|
||||
state: WalletState.ContentState,
|
||||
wallets: List<UserWallet>,
|
||||
selectedWallet: UserWallet,
|
||||
): Action {
|
||||
val selectedWalletName = selectedWallet.name
|
||||
|
||||
if (state.getPrevSelectedWalletName() != selectedWalletName) {
|
||||
return Action.UpdateWalletName(selectedWalletName)
|
||||
}
|
||||
|
||||
if (state is WalletLockedState && !selectedWallet.isLocked) {
|
||||
return Action.UnlockWallet(
|
||||
selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId),
|
||||
selectedWallet = selectedWallet,
|
||||
unlockedWallets = wallets.filterNot(UserWallet::isLocked),
|
||||
)
|
||||
}
|
||||
|
||||
return Action.Unknown
|
||||
}
|
||||
|
||||
private fun WalletState.ContentState.getPrevSelectedWalletName(): String {
|
||||
val prevSelectedWalletIndex = walletsListConfig.selectedWalletIndex
|
||||
val prevSelectedWallet = walletsListConfig.wallets.getOrNull(prevSelectedWalletIndex)
|
||||
?: error("Previous selected wallet is not found")
|
||||
|
||||
return prevSelectedWallet.title
|
||||
}
|
||||
|
||||
private fun List<UserWallet>.indexOfWallet(id: UserWalletId): Int {
|
||||
val selectedIndex = indexOfFirst { it.walletId == id }
|
||||
|
||||
return if (selectedIndex == -1) {
|
||||
error("Wallets don't contain a wallet with id: $id")
|
||||
} else {
|
||||
selectedIndex
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Action {
|
||||
|
||||
data class InitialWallets(val selectedWalletIndex: Int) : Action()
|
||||
|
||||
data class UpdateWalletName(val name: String) : Action()
|
||||
|
||||
data class UnlockWallet(
|
||||
val selectedWalletIndex: Int,
|
||||
val selectedWallet: UserWallet,
|
||||
val unlockedWallets: List<UserWallet>,
|
||||
) : Action()
|
||||
|
||||
data class DeleteWallet(
|
||||
val selectedWalletId: UserWalletId,
|
||||
val selectedWalletIndex: Int,
|
||||
val deletedWalletId: UserWalletId,
|
||||
) : Action()
|
||||
|
||||
data class AddWallet(val selectedWalletIndex: Int) : Action()
|
||||
|
||||
object Unknown : Action()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue