Updated on 2026-08-14
This commit is contained in:
commit
cae7844408
49 changed files with 537 additions and 273 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit 098662beb5b0123b11f5ee4873d4bd667ac93c73
|
||||
Subproject commit 8285f8d2896d2994337f55a7b3a54fd5acc24304
|
||||
|
|
@ -204,6 +204,16 @@ internal object YieldSupplyDomainModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideYieldSupplyEnterStatusFlowUseCase(
|
||||
yieldSupplyRepository: YieldSupplyRepository,
|
||||
): YieldSupplyEnterStatusFlowUseCase {
|
||||
return YieldSupplyEnterStatusFlowUseCase(
|
||||
yieldSupplyRepository = yieldSupplyRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideYieldSupplyGetShouldShowMainPromoUseCase(
|
||||
|
|
|
|||
|
|
@ -95,8 +95,6 @@ private fun TrendingArticle(
|
|||
TangemTheme.colors.text.primary1
|
||||
},
|
||||
style = TangemTheme.typography.h3,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -47,10 +47,6 @@
|
|||
"name": "TANGEM_PAY_ENABLED",
|
||||
"version": "5.31.0"
|
||||
},
|
||||
{
|
||||
"name": "TANGEM_PAY_ENTRYPOINT_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "NEW_TOKEN_RECEIVE_ENABLED",
|
||||
"version": "5.28.0"
|
||||
|
|
|
|||
|
|
@ -1469,6 +1469,9 @@
|
|||
<string name="tangem_pay_freeze_card_freeze">Freeze</string>
|
||||
<string name="tangem_pay_freeze_card_success">Your card is frozen.</string>
|
||||
<string name="tangem_pay_get_help">Get Help</string>
|
||||
<string name="tangem_pay_history_item_spend_mc_declined_reason">Reason: %s</string>
|
||||
<string name="tangem_pay_history_item_spend_mc_title_format" formatted="false">%s · %s</string>
|
||||
<string name="tangem_pay_history_item_spend_mcc">MCC %s</string>
|
||||
<string name="tangem_pay_other">Other</string>
|
||||
<string name="tangem_pay_rooted_device_subtitle">Unable to use on rooted devices</string>
|
||||
<string name="tangem_pay_status_completed">Completed</string>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.material3.ButtonColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -54,12 +55,16 @@ fun TextButtonIconStart(
|
|||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
colors: ButtonColors = TangemButtonsDefaults.defaultTextButtonColors,
|
||||
tint: Color? = null,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
TangemButton(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
icon = TangemButtonIconPosition.Start(iconResId),
|
||||
icon = TangemButtonIconPosition.Start(
|
||||
iconResId = iconResId,
|
||||
iconTint = tint,
|
||||
),
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
showProgress = false,
|
||||
|
|
@ -117,13 +122,21 @@ fun PrimaryButtonIconEnd(
|
|||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
size: TangemButtonSize = TangemButtonSize.Default,
|
||||
tint: Color? = null,
|
||||
showProgress: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
TangemButton(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
icon = if (iconResId != null) TangemButtonIconPosition.End(iconResId) else TangemButtonIconPosition.None,
|
||||
icon = if (iconResId != null) {
|
||||
TangemButtonIconPosition.End(
|
||||
iconResId = iconResId,
|
||||
iconTint = tint,
|
||||
)
|
||||
} else {
|
||||
TangemButtonIconPosition.None
|
||||
},
|
||||
onClick = onClick,
|
||||
colors = TangemButtonsDefaults.primaryButtonColors,
|
||||
enabled = enabled,
|
||||
|
|
@ -145,11 +158,15 @@ fun PrimaryButtonIconEndTwoLines(
|
|||
showProgress: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
additionalText: String? = null,
|
||||
tint: Color? = null,
|
||||
) {
|
||||
TangemButton(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
icon = TangemButtonIconPosition.End(iconResId),
|
||||
icon = TangemButtonIconPosition.End(
|
||||
iconResId = iconResId,
|
||||
iconTint = tint,
|
||||
),
|
||||
onClick = onClick,
|
||||
colors = TangemButtonsDefaults.primaryButtonColors,
|
||||
enabled = enabled,
|
||||
|
|
@ -171,13 +188,17 @@ fun PrimaryButtonIconStart(
|
|||
modifier: Modifier = Modifier,
|
||||
showProgress: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
tint: Color? = null,
|
||||
size: TangemButtonSize = TangemButtonSize.Default,
|
||||
shape: Shape = size.toShape(),
|
||||
) {
|
||||
TangemButton(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
icon = TangemButtonIconPosition.Start(iconResId),
|
||||
icon = TangemButtonIconPosition.Start(
|
||||
iconResId = iconResId,
|
||||
iconTint = tint,
|
||||
),
|
||||
onClick = onClick,
|
||||
colors = TangemButtonsDefaults.primaryButtonColors,
|
||||
enabled = enabled,
|
||||
|
|
@ -225,13 +246,17 @@ fun SecondaryButtonIconEnd(
|
|||
modifier: Modifier = Modifier,
|
||||
showProgress: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
tint: Color? = null,
|
||||
size: TangemButtonSize = TangemButtonSize.Default,
|
||||
shape: Shape = size.toShape(),
|
||||
) {
|
||||
TangemButton(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
icon = TangemButtonIconPosition.End(iconResId),
|
||||
icon = TangemButtonIconPosition.End(
|
||||
iconResId = iconResId,
|
||||
iconTint = tint,
|
||||
),
|
||||
onClick = onClick,
|
||||
colors = TangemButtonsDefaults.secondaryButtonColors,
|
||||
enabled = enabled,
|
||||
|
|
@ -253,13 +278,17 @@ fun SecondaryButtonIconStart(
|
|||
modifier: Modifier = Modifier,
|
||||
showProgress: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
iconTint: Color? = null,
|
||||
size: TangemButtonSize = TangemButtonSize.Default,
|
||||
shape: Shape = size.toShape(),
|
||||
) {
|
||||
TangemButton(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
icon = TangemButtonIconPosition.Start(iconResId),
|
||||
icon = TangemButtonIconPosition.Start(
|
||||
iconResId = iconResId,
|
||||
iconTint = iconTint,
|
||||
),
|
||||
onClick = onClick,
|
||||
colors = TangemButtonsDefaults.secondaryButtonColors,
|
||||
enabled = enabled,
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ fun TangemButton(
|
|||
.padding(vertical = 2.dp)
|
||||
.testTag(BaseButtonTestTags.ICON),
|
||||
painter = painterResource(id = iconResId),
|
||||
tint = colors.contentColor(enabled = enabled).value,
|
||||
tint = icon.iconTint ?: colors.contentColor(enabled = enabled).value,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,17 +2,27 @@ package com.tangem.core.ui.components.buttons.common
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Immutable
|
||||
sealed interface TangemButtonIconPosition {
|
||||
val iconResId: Int?
|
||||
val iconTint: Color?
|
||||
|
||||
data class Start(@DrawableRes override val iconResId: Int) : TangemButtonIconPosition
|
||||
data class Start(
|
||||
@DrawableRes override val iconResId: Int,
|
||||
override val iconTint: Color? = null,
|
||||
) : TangemButtonIconPosition
|
||||
|
||||
data class End(@DrawableRes override val iconResId: Int) : TangemButtonIconPosition
|
||||
data class End(
|
||||
@DrawableRes override val iconResId: Int,
|
||||
override val iconTint: Color? = null,
|
||||
) : TangemButtonIconPosition
|
||||
|
||||
data object None : TangemButtonIconPosition {
|
||||
@DrawableRes
|
||||
override val iconResId: Int? = null
|
||||
|
||||
override val iconTint: Color? = null
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,6 @@ import androidx.compose.ui.res.painterResource
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.BuildConfig
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerW4
|
||||
import com.tangem.core.ui.components.account.AccountCharIcon
|
||||
|
|
@ -33,9 +32,9 @@ import com.tangem.core.ui.components.token.state.TokenItemState
|
|||
import com.tangem.core.ui.components.tokenlist.internal.GroupTitleItem
|
||||
import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.ProvideSharedTransitionScope
|
||||
|
||||
/**
|
||||
* Multi-currency content item
|
||||
|
|
@ -69,7 +68,7 @@ fun TokenListItem(state: TokensListItemUM, isBalanceHidden: Boolean, modifier: M
|
|||
@Suppress("MagicNumber", "ReusedModifierInstance", "LongMethod")
|
||||
@Composable
|
||||
fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
|
||||
SharedTransitionLayout(modifier) {
|
||||
ProvideSharedTransitionScope(modifier) {
|
||||
val iconSharedContentState = rememberSharedContentState(key = "icon_${state.id}")
|
||||
val titleSharedContentState = rememberSharedContentState(key = "title_${state.id}")
|
||||
val boundsTransform = BoundsTransform { _, _ -> tween(350) }
|
||||
|
|
@ -101,13 +100,11 @@ fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolea
|
|||
state = iconState,
|
||||
withFixedSize = false,
|
||||
modifier = modifier
|
||||
.conditional(BuildConfig.DEBUG.not()) {
|
||||
sharedBounds(
|
||||
sharedContentState = iconSharedContentState,
|
||||
animatedVisibilityScope = animatedContentScope,
|
||||
boundsTransform = boundsTransform,
|
||||
)
|
||||
},
|
||||
.sharedBounds(
|
||||
sharedContentState = iconSharedContentState,
|
||||
animatedVisibilityScope = animatedContentScope,
|
||||
boundsTransform = boundsTransform,
|
||||
),
|
||||
)
|
||||
},
|
||||
title = { modifier: Modifier ->
|
||||
|
|
@ -126,14 +123,12 @@ fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolea
|
|||
state = state.tokenItemUM.titleState,
|
||||
textStyle = textStyle.copy(fontSize = textSize.sp),
|
||||
modifier = modifier
|
||||
.conditional(BuildConfig.DEBUG.not()) {
|
||||
sharedBounds(
|
||||
sharedContentState = titleSharedContentState,
|
||||
animatedVisibilityScope = animatedContentScope,
|
||||
boundsTransform = boundsTransform,
|
||||
resizeMode = scaleToBounds(ContentScale.Fit, Alignment.CenterStart),
|
||||
)
|
||||
},
|
||||
.sharedBounds(
|
||||
sharedContentState = titleSharedContentState,
|
||||
animatedVisibilityScope = animatedContentScope,
|
||||
boundsTransform = boundsTransform,
|
||||
resizeMode = scaleToBounds(ContentScale.Fit, Alignment.CenterStart),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.tangem.core.ui.utils.DateTimeFormatters.dateMMMdd
|
|||
import com.tangem.core.ui.utils.DateTimeFormatters.dateTimeFormatter
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters.dateYYYY
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import org.joda.time.format.DateTimeFormat
|
||||
import org.joda.time.format.DateTimeFormatter
|
||||
import org.joda.time.format.DateTimeFormatterBuilder
|
||||
|
|
@ -18,7 +17,7 @@ object DateTimeFormatters {
|
|||
/**
|
||||
* Determine if the time is in 12-hour format ("10:00 PM") for the current locale.
|
||||
*/
|
||||
val is12HourFormat by lazy {
|
||||
private val is12HourFormat by lazy {
|
||||
/**
|
||||
* Two SS means, SHORT style for date and time.
|
||||
* If pattern contains "a", it means time is in 12 hour format.
|
||||
|
|
@ -103,17 +102,15 @@ object DateTimeFormatters {
|
|||
* Local full date formatter (e.g., "dd MMMM, HH:mm")
|
||||
*/
|
||||
val localFullDate: DateTimeFormatter by lazy {
|
||||
val locale = Locale.getDefault()
|
||||
val datePattern = DateFormat.getBestDateTimePattern(locale, "dd MMMM")
|
||||
val timeSkeleton = if (is12HourFormat) "h:mm a" else "HH:mm"
|
||||
val timePattern = DateFormat.getBestDateTimePattern(locale, timeSkeleton)
|
||||
val fullPattern = "$datePattern, $timePattern"
|
||||
DateTimeFormatterBuilder()
|
||||
.appendDayOfMonth(2)
|
||||
.appendLiteral(' ')
|
||||
.appendMonthOfYearText()
|
||||
.appendLiteral(", ")
|
||||
.appendHourOfDay(2)
|
||||
.appendLiteral(':')
|
||||
.appendMinuteOfHour(2)
|
||||
.appendPattern(fullPattern)
|
||||
.toFormatter()
|
||||
.withLocale(Locale.getDefault())
|
||||
.withZone(DateTimeZone.getDefault())
|
||||
.withLocale(locale)
|
||||
}
|
||||
|
||||
fun formatDate(date: DateTime, formatter: DateTimeFormatter = dateFormatter): String {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import androidx.compose.animation.SharedTransitionLayout
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun TangemSharedTransitionLayout(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable SharedTransitionScope.() -> Unit,
|
||||
) {
|
||||
SharedTransitionLayout(modifier) {
|
||||
val sharedTransitionScope = this
|
||||
CompositionLocalProvider(
|
||||
LocalSharedTransitionScope provides sharedTransitionScope,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProvideSharedTransitionScope(modifier: Modifier = Modifier, content: @Composable SharedTransitionScope.() -> Unit) {
|
||||
val sharedTransitionScope = LocalSharedTransitionScope.current
|
||||
Box(modifier) {
|
||||
sharedTransitionScope.content()
|
||||
}
|
||||
}
|
||||
|
||||
private val LocalSharedTransitionScope = staticCompositionLocalOf<SharedTransitionScope> {
|
||||
error("No SharedTransitionScope provided")
|
||||
}
|
||||
11
core/ui/src/main/res/drawable/ic_heart_filled_20.xml
Normal file
11
core/ui/src/main/res/drawable/ic_heart_filled_20.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M13.75,1.75C16.73,1.75 19.084,4.093 19.084,7.083C19.084,8.9 18.257,10.495 16.961,12.081C15.673,13.657 13.844,15.315 11.713,17.247L10.505,18.347L10,18.806L9.495,18.347L8.288,17.247C6.157,15.315 4.327,13.657 3.039,12.081C1.743,10.495 0.917,8.9 0.917,7.083C0.917,4.093 3.27,1.75 6.25,1.75C7.646,1.75 8.986,2.29 10,3.174C11.013,2.29 12.353,1.75 13.75,1.75Z"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#FF3333"
|
||||
android:strokeColor="#FF3333"/>
|
||||
</vector>
|
||||
|
|
@ -48,15 +48,17 @@ internal class DefaultMainAccountTokensMigration(
|
|||
exception
|
||||
}
|
||||
val mainAccount = findAccount(response = response, derivationIndex = DerivationIndex.Main)
|
||||
val notMainAccounts = response.accounts
|
||||
val customAccounts = response.accounts
|
||||
.filterNot { accountDTO -> accountDTO.derivationIndex.toDerivationIndex().isMain }
|
||||
|
||||
if (notMainAccounts.isEmpty()) {
|
||||
if (customAccounts.isEmpty()) {
|
||||
Timber.i("There is only the Main account. Nothing to migrate")
|
||||
return@either response
|
||||
}
|
||||
|
||||
val customAccountIndexes = customAccounts.mapTo(hashSetOf(), WalletAccountDTO::derivationIndex)
|
||||
val unassignedTokens = mainAccount.groupUnassignedTokens()
|
||||
.filter { it.key.value in customAccountIndexes }
|
||||
|
||||
if (unassignedTokens.isEmpty()) {
|
||||
Timber.i("No unassigned tokens found for migration")
|
||||
|
|
@ -64,7 +66,7 @@ internal class DefaultMainAccountTokensMigration(
|
|||
}
|
||||
|
||||
var updatedMainAccount = mainAccount
|
||||
val assignedTokensAccounts = notMainAccounts.mapNotNull { accountDTO ->
|
||||
val assignedTokensAccounts = customAccounts.mapNotNull { accountDTO ->
|
||||
val derivationIndex = accountDTO.derivationIndex.toDerivationIndex()
|
||||
val tokensForAccount = unassignedTokens[derivationIndex]
|
||||
if (tokensForAccount.isNullOrEmpty()) return@mapNotNull null
|
||||
|
|
|
|||
|
|
@ -376,6 +376,7 @@ class DefaultMainAccountTokensMigrationTest {
|
|||
derivationIndex = DerivationIndex.Main.value,
|
||||
tokens = listOf(
|
||||
createBitcoin(accountIndex = 0),
|
||||
createBitcoin(accountIndex = 10),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ internal class DefaultNewsRepository(
|
|||
articlesToUpdate.map { article ->
|
||||
val isViewed = viewedFlags[article.id] == true
|
||||
article.copy(viewed = isViewed)
|
||||
}.sortedBy { it.viewed }
|
||||
}
|
||||
}
|
||||
.map<List<ShortArticle>, Either<Throwable, List<ShortArticle>>> { it.right() }
|
||||
.catch { emit(it.left()) }
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ class DefaultGaslessTransactionRepository(
|
|||
}
|
||||
|
||||
private companion object {
|
||||
val BASE_GAS_FOR_TRANSACTION: BigInteger = BigInteger("100000")
|
||||
val BASE_GAS_FOR_TRANSACTION: BigInteger = BigInteger("60000")
|
||||
val EMPTY_ADDRESSES = emptySet<String>()
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ import com.tangem.domain.pay.TangemPayEligibilityManager
|
|||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.*
|
||||
|
|
@ -23,7 +22,6 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
|
|||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
) : TangemPayEligibilityManager {
|
||||
|
||||
|
|
@ -128,8 +126,6 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun checkTangemPayEligibility(): Boolean {
|
||||
if (!tangemPayFeatureToggles.isEntryPointsEnabled) return true
|
||||
|
||||
return onboardingRepository.getCustomerEligibility() || onboardingRepository.checkCustomerEligibility()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
|
|||
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.security.DeviceSecurityInfoProvider
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
|
@ -78,14 +77,12 @@ internal interface TangemPayDataModule {
|
|||
customerOrderRepository: CustomerOrderRepository,
|
||||
tangemPayOnboardingRepository: OnboardingRepository,
|
||||
eligibilityManager: TangemPayEligibilityManager,
|
||||
tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
deviceSecurity: DeviceSecurityInfoProvider,
|
||||
): TangemPayMainScreenCustomerInfoUseCase {
|
||||
return TangemPayMainScreenCustomerInfoUseCase(
|
||||
onboardingRepository = repository,
|
||||
customerOrderRepository = customerOrderRepository,
|
||||
eligibilityManager = eligibilityManager,
|
||||
tangemPayFeatureToggles = tangemPayFeatureToggles,
|
||||
deviceSecurity = deviceSecurity,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ internal class TangemPayTxHistoryItemConverter(moshi: Moshi) :
|
|||
merchantCategory = spend.merchantCategory,
|
||||
status = TangemPayTxHistoryItemStatusConverter.convert(spend.status),
|
||||
enrichedMerchantIconUrl = spend.enrichedMerchantIcon,
|
||||
declinedReason = spend.declinedReason,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -280,6 +280,12 @@ internal class DefaultWalletManagersFacade @Inject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
val gaslessFeeAddresses = try {
|
||||
gaslessTransactionRepository.getGaslessFeeAddresses()
|
||||
} catch (error: Throwable) {
|
||||
Timber.e(error, "Failed to load gasless fee addresses; falling back to empty set")
|
||||
emptySet()
|
||||
}
|
||||
return when (itemsResult) {
|
||||
is Result.Success -> PaginationWrapper(
|
||||
currentPage = sdkPageConverter.convert(page),
|
||||
|
|
@ -287,7 +293,7 @@ internal class DefaultWalletManagersFacade @Inject constructor(
|
|||
items = SdkTransactionHistoryItemConverter(
|
||||
smartContractMethods = readSmartContractMethods(),
|
||||
yieldSupplyAddresses = YIELD_SUPPLY_ADDRESSES,
|
||||
gaslessFeeAddresses = gaslessTransactionRepository.getGaslessFeeAddresses(),
|
||||
gaslessFeeAddresses = gaslessFeeAddresses,
|
||||
).convertList(itemsResult.data.items),
|
||||
)
|
||||
is Result.Failure -> error(itemsResult.error.message ?: itemsResult.error.customMessage)
|
||||
|
|
|
|||
|
|
@ -27,9 +27,10 @@ import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
|
|||
import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
internal class DefaultYieldSupplyRepository(
|
||||
private val yieldSupplyApi: YieldSupplyApi,
|
||||
|
|
@ -40,7 +41,7 @@ internal class DefaultYieldSupplyRepository(
|
|||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : YieldSupplyRepository {
|
||||
|
||||
private val statusMap: MutableMap<String, YieldSupplyPendingStatus> = ConcurrentHashMap()
|
||||
private val statusMapFlow = MutableStateFlow<Map<String, YieldSupplyPendingStatus>>(emptyMap())
|
||||
|
||||
override suspend fun getCachedMarkets(): List<YieldMarketToken>? = withContext(dispatchers.io) {
|
||||
val cache = store.getSyncOrNull().orEmpty()
|
||||
|
|
@ -129,10 +130,12 @@ internal class DefaultYieldSupplyRepository(
|
|||
yieldSupplyPendingStatus: YieldSupplyPendingStatus?,
|
||||
) {
|
||||
val key = getTokenProtocolStatusKey(userWalletId, cryptoCurrency)
|
||||
if (yieldSupplyPendingStatus != null) {
|
||||
statusMap[key] = yieldSupplyPendingStatus
|
||||
} else {
|
||||
statusMap.remove(key)
|
||||
statusMapFlow.update { currentMap ->
|
||||
if (yieldSupplyPendingStatus != null) {
|
||||
currentMap + (key to yieldSupplyPendingStatus)
|
||||
} else {
|
||||
currentMap - key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -153,7 +156,15 @@ internal class DefaultYieldSupplyRepository(
|
|||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): YieldSupplyPendingStatus? {
|
||||
return statusMap[getTokenProtocolStatusKey(userWalletId, cryptoCurrency)]
|
||||
return statusMapFlow.value[getTokenProtocolStatusKey(userWalletId, cryptoCurrency)]
|
||||
}
|
||||
|
||||
override fun getTokenProtocolPendingStatusFlow(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): Flow<YieldSupplyPendingStatus?> {
|
||||
val key = getTokenProtocolStatusKey(userWalletId, cryptoCurrency)
|
||||
return statusMapFlow.map { it[key] }
|
||||
}
|
||||
|
||||
private fun List<YieldMarketToken>.enrichNetworkIds(): List<YieldMarketToken> {
|
||||
|
|
|
|||
|
|
@ -133,9 +133,10 @@ internal class TokenFeeCalculator(
|
|||
is Fee.Ethereum.TokenCurrency -> raiseIllegalStateError("initialFee could only be native")
|
||||
}
|
||||
|
||||
val feeInNativeCurrency = maxTokenFeeGas
|
||||
.multiply(maxFeePerGas)
|
||||
.multiply(GAS_PRICE_MULTIPLIER.toBigInteger())
|
||||
val feeInNativeCurrency = BigDecimal(maxTokenFeeGas.multiply(maxFeePerGas))
|
||||
.multiply(BigDecimal(GAS_PRICE_MULTIPLIER))
|
||||
.setScale(0, RoundingMode.UP)
|
||||
.toBigInteger()
|
||||
|
||||
val nativeFiatRate = nativeCurrencyStatus.value.fiatRate ?: raiseIllegalStateError("fiatRate is null")
|
||||
val tokenFiatRate = tokenForPayFeeStatus.value.fiatRate ?: raiseIllegalStateError("fiatRate is null")
|
||||
|
|
@ -201,7 +202,7 @@ internal class TokenFeeCalculator(
|
|||
/** Amount in token units for fee transfer */
|
||||
const val FEE_TRANSFER_AMOUNT = 0.01 // calculate using decimals
|
||||
/** Gas price safety multiplier for fee calculation */
|
||||
const val GAS_PRICE_MULTIPLIER = 2
|
||||
const val GAS_PRICE_MULTIPLIER = 1.5
|
||||
const val PERCENT_TO_INCREASE_TOKEN_PRICE = 1
|
||||
const val PERCENT_TO_INCREASE_TRANSFER_GASLIMIT = 10
|
||||
|
||||
|
|
|
|||
|
|
@ -363,7 +363,7 @@ class TokenFeeCalculatorTest {
|
|||
// Expected
|
||||
|
||||
val expectedAmount = Amount(
|
||||
value = BigDecimal("37.400000000000000000000000000000000000"),
|
||||
value = BigDecimal("28.050000000000000000000000000000000000"),
|
||||
token = Token(
|
||||
name = "USDC",
|
||||
symbol = "USDC",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ sealed class TangemPayTxHistoryItem {
|
|||
val merchantCategory: String?,
|
||||
val status: Status,
|
||||
val enrichedMerchantIconUrl: String?,
|
||||
val declinedReason: String?,
|
||||
) : TangemPayTxHistoryItem()
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import com.tangem.domain.pay.model.*
|
|||
import com.tangem.domain.pay.repository.CustomerOrderRepository
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.security.DeviceSecurityInfoProvider
|
||||
import com.tangem.security.isSecurityExposed
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
|
@ -21,7 +20,6 @@ class TangemPayMainScreenCustomerInfoUseCase(
|
|||
private val onboardingRepository: OnboardingRepository,
|
||||
private val customerOrderRepository: CustomerOrderRepository,
|
||||
private val eligibilityManager: TangemPayEligibilityManager,
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
private val deviceSecurity: DeviceSecurityInfoProvider,
|
||||
) {
|
||||
|
||||
|
|
@ -44,7 +42,7 @@ class TangemPayMainScreenCustomerInfoUseCase(
|
|||
.fold(
|
||||
ifLeft = { error ->
|
||||
Timber.tag(TAG).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}")
|
||||
if (error is VisaApiError.NotPaeraCustomer && tangemPayFeatureToggles.isEntryPointsEnabled) {
|
||||
if (error is VisaApiError.NotPaeraCustomer) {
|
||||
showOnboardingBannerIfEligible(userWalletId)
|
||||
} else {
|
||||
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
|
||||
|
|
@ -62,13 +60,8 @@ class TangemPayMainScreenCustomerInfoUseCase(
|
|||
.map(MainCustomerInfoContentState::Content)
|
||||
updateState(userWalletId, result)
|
||||
} else {
|
||||
if (tangemPayFeatureToggles.isEntryPointsEnabled) {
|
||||
// if there's no tangem pay, check eligibility and show onboarding banner
|
||||
showOnboardingBannerIfEligible(userWalletId)
|
||||
} else {
|
||||
// ignore if there's no TangemPay
|
||||
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
|
||||
}
|
||||
// if there's no tangem pay, check eligibility and show onboarding banner
|
||||
showOnboardingBannerIfEligible(userWalletId)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -104,6 +104,18 @@ interface YieldSupplyRepository {
|
|||
cryptoCurrency: CryptoCurrency,
|
||||
): YieldSupplyPendingStatus?
|
||||
|
||||
/**
|
||||
* Observe the pending status for the given wallet and currency as a [Flow].
|
||||
*
|
||||
* @param userWalletId the wallet to observe
|
||||
* @param cryptoCurrency the currency or token to observe
|
||||
* @return a [Flow] emitting the current pending status or null if none exists
|
||||
*/
|
||||
fun getTokenProtocolPendingStatusFlow(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): Flow<YieldSupplyPendingStatus?>
|
||||
|
||||
fun getShouldShowYieldPromoBanner(): Flow<Boolean>
|
||||
|
||||
suspend fun setShouldShowYieldPromoBanner(shouldShow: Boolean)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.domain.yield.supply.usecase
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.yield.supply.YieldSupplyRepository
|
||||
import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class YieldSupplyEnterStatusFlowUseCase(
|
||||
private val yieldSupplyRepository: YieldSupplyRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Flow<YieldSupplyPendingStatus?> {
|
||||
return yieldSupplyRepository.getTokenProtocolPendingStatusFlow(userWalletId, cryptoCurrency)
|
||||
}
|
||||
}
|
||||
|
|
@ -24,28 +24,24 @@ class YieldSupplyEnterStatusUseCase(
|
|||
.toSet()
|
||||
val hasPendingTx = status?.txIds?.any { it in pendingTxHashes } == true
|
||||
|
||||
if (hasPendingTx) {
|
||||
status
|
||||
val isActive = cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true
|
||||
val isExpired = status != null &&
|
||||
System.currentTimeMillis() - status.createdAt > STATUS_EXPIRATION_MS
|
||||
val shouldClearStatus = when {
|
||||
isActive && status is YieldSupplyPendingStatus.Enter -> true
|
||||
!isActive && status is YieldSupplyPendingStatus.Exit -> true
|
||||
isExpired && !hasPendingTx -> true
|
||||
else -> false
|
||||
}
|
||||
if (shouldClearStatus) {
|
||||
yieldSupplyRepository.saveTokenProtocolPendingStatus(
|
||||
userWalletId,
|
||||
cryptoCurrencyStatus.currency,
|
||||
null,
|
||||
)
|
||||
null
|
||||
} else {
|
||||
val isActive = cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true
|
||||
val isExpired = status != null &&
|
||||
System.currentTimeMillis() - status.createdAt > STATUS_EXPIRATION_MS
|
||||
val shouldClearStatus = when {
|
||||
isExpired -> true
|
||||
isActive && status is YieldSupplyPendingStatus.Exit -> false
|
||||
!isActive && status is YieldSupplyPendingStatus.Enter -> false
|
||||
else -> true
|
||||
}
|
||||
if (shouldClearStatus) {
|
||||
yieldSupplyRepository.saveTokenProtocolPendingStatus(
|
||||
userWalletId,
|
||||
cryptoCurrencyStatus.currency,
|
||||
null,
|
||||
)
|
||||
null
|
||||
} else {
|
||||
status
|
||||
}
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,16 +91,13 @@ class YieldSupplyEnterStatusUseCaseTest {
|
|||
coEvery {
|
||||
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus.currency)
|
||||
} returns emptyList()
|
||||
coEvery {
|
||||
yieldSupplyRepository.saveTokenProtocolPendingStatus(userWalletId, token, null)
|
||||
} returns Unit
|
||||
|
||||
val result = useCase(userWalletId, cryptoStatus)
|
||||
|
||||
assertThat(result.isRight()).isTrue()
|
||||
val value = (result as Either.Right).value
|
||||
assertThat(value).isNull()
|
||||
coVerify { yieldSupplyRepository.saveTokenProtocolPendingStatus(userWalletId, token, null) }
|
||||
coVerify(exactly = 0) { yieldSupplyRepository.saveTokenProtocolPendingStatus(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -147,7 +144,7 @@ class YieldSupplyEnterStatusUseCaseTest {
|
|||
@Test
|
||||
fun `GIVEN exit status with pending tx WHEN invoke THEN returns status`() = runTest {
|
||||
val token = createToken()
|
||||
val cryptoStatus = createStatus(token)
|
||||
val cryptoStatus = createStatus(token, isActive = true)
|
||||
val pendingTxHash = "0xexit456"
|
||||
val status = YieldSupplyPendingStatus.Exit(txIds = listOf(pendingTxHash))
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ import com.tangem.features.details.entity.SelectEmailFeedbackTypeBS
|
|||
import com.tangem.features.details.utils.ItemsBuilder
|
||||
import com.tangem.features.details.utils.SocialsBuilder
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -71,7 +70,6 @@ internal class DetailsModel @Inject constructor(
|
|||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val tangemPayEligibilityManager: TangemPayEligibilityManager,
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
) : Model() {
|
||||
|
||||
private val params: DetailsComponent.Params = paramsContainer.require()
|
||||
|
|
@ -249,7 +247,6 @@ internal class DetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun addTangemPayItemIfEligible() {
|
||||
if (!tangemPayFeatureToggles.isEntryPointsEnabled) return
|
||||
modelScope.launch {
|
||||
val isEligible = tangemPayEligibilityManager
|
||||
.getEligibleWallets(shouldExcludePaeraCustomers = true)
|
||||
|
|
|
|||
|
|
@ -16,15 +16,20 @@ import androidx.compose.material3.Icon
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -141,6 +146,7 @@ private fun ArticleDetail(
|
|||
relatedTokensUM: RelatedTokensUM,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val density = LocalDensity.current
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
val pagerHeight = 32.dp
|
||||
|
|
@ -184,23 +190,21 @@ private fun ArticleDetail(
|
|||
|
||||
SpacerH(24.dp)
|
||||
|
||||
if (article.isLiked) {
|
||||
PrimaryButtonIconStart(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
iconResId = R.drawable.ic_heart_20,
|
||||
text = stringResourceSafe(R.string.news_like),
|
||||
size = TangemButtonSize.RoundedAction,
|
||||
onClick = { onLikeClick() },
|
||||
)
|
||||
} else {
|
||||
SecondaryButtonIconStart(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
iconResId = R.drawable.ic_heart_20,
|
||||
text = stringResourceSafe(R.string.news_like),
|
||||
size = TangemButtonSize.RoundedAction,
|
||||
onClick = { onLikeClick() },
|
||||
)
|
||||
}
|
||||
SecondaryButtonIconStart(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
iconResId = if (article.isLiked) {
|
||||
R.drawable.ic_heart_filled_20
|
||||
} else {
|
||||
R.drawable.ic_heart_20
|
||||
},
|
||||
iconTint = Color.Unspecified,
|
||||
text = stringResourceSafe(R.string.news_like),
|
||||
size = TangemButtonSize.RoundedAction,
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onLikeClick()
|
||||
},
|
||||
)
|
||||
|
||||
RelatedTokensBlock(
|
||||
relatedTokensUM = relatedTokensUM,
|
||||
|
|
@ -375,16 +379,21 @@ private fun RelatedNewsItem(relatedArticle: RelatedArticleUM, modifier: Modifier
|
|||
@Composable
|
||||
private fun PreviewNewsDetailsContent() {
|
||||
TangemThemePreview {
|
||||
NewsDetailsContent(
|
||||
state = NewsDetailsUM(
|
||||
articlesStateUM = ArticlesStateUM.Content,
|
||||
articles = MockArticlesFactory.createMockArticles(),
|
||||
selectedArticleIndex = 0,
|
||||
onShareClick = {},
|
||||
onLikeClick = {},
|
||||
onBackClick = {},
|
||||
onArticleIndexChanged = {},
|
||||
),
|
||||
)
|
||||
val background = TangemTheme.colors.background.tertiary
|
||||
CompositionLocalProvider(
|
||||
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
|
||||
) {
|
||||
NewsDetailsContent(
|
||||
state = NewsDetailsUM(
|
||||
articlesStateUM = ArticlesStateUM.Content,
|
||||
articles = MockArticlesFactory.createMockArticles(),
|
||||
selectedArticleIndex = 0,
|
||||
onShareClick = {},
|
||||
onLikeClick = {},
|
||||
onBackClick = {},
|
||||
onArticleIndexChanged = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -44,22 +44,30 @@ sealed class CommonSendAnalyticEvents(
|
|||
data class FeeScreenOpened(
|
||||
val categoryName: String,
|
||||
val source: CommonSendSource,
|
||||
val blockchain: String,
|
||||
val token: String,
|
||||
) : CommonSendAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Fee Screen Opened",
|
||||
params = mapOf(
|
||||
SOURCE to source.analyticsName,
|
||||
BLOCKCHAIN to blockchain,
|
||||
TOKEN_PARAM to token,
|
||||
),
|
||||
)
|
||||
|
||||
data class FeeSummaryScreenOpened(
|
||||
val categoryName: String,
|
||||
val source: CommonSendSource,
|
||||
val blockchain: String,
|
||||
val token: String,
|
||||
) : CommonSendAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Fee Summary Screen Opened",
|
||||
params = mapOf(
|
||||
SOURCE to source.analyticsName,
|
||||
BLOCKCHAIN to blockchain,
|
||||
TOKEN_PARAM to token,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -67,12 +75,14 @@ sealed class CommonSendAnalyticEvents(
|
|||
val categoryName: String,
|
||||
val source: CommonSendSource,
|
||||
val availableTokens: String,
|
||||
val blockchain: String,
|
||||
) : CommonSendAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Fee Token Screen Opened",
|
||||
params = mapOf(
|
||||
SOURCE to source.analyticsName,
|
||||
"Available Fee" to availableTokens,
|
||||
BLOCKCHAIN to blockchain,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
|
|||
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TOKEN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
|
||||
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.CommonSendSource
|
||||
|
||||
sealed class CommonSendFeeAnalyticEvents(
|
||||
|
|
@ -38,11 +39,13 @@ sealed class CommonSendFeeAnalyticEvents(
|
|||
data class CustomFeeButtonClicked(
|
||||
override val categoryName: String,
|
||||
val blockchain: String,
|
||||
val token: String,
|
||||
) : CommonSendFeeAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Custom Fee Clicked",
|
||||
params = mapOf(
|
||||
BLOCKCHAIN to blockchain,
|
||||
TOKEN_PARAM to token,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class FeeSelectorLogic @AssistedInject constructor(
|
||||
|
|
@ -122,6 +123,7 @@ internal class FeeSelectorLogic @AssistedInject constructor(
|
|||
CommonSendFeeAnalyticEvents.CustomFeeButtonClicked(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
blockchain = params.cryptoCurrencyStatus.currency.network.name,
|
||||
token = params.cryptoCurrencyStatus.currency.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -265,10 +267,21 @@ internal class FeeSelectorLogic @AssistedInject constructor(
|
|||
private suspend fun populateExtendedFee(
|
||||
fee: TransactionFeeExtended,
|
||||
): Either<GetFeeError, LoadedFeeResult.Extended> = either {
|
||||
val selectedToken = getSelectedTokenStatus(fee.feeTokenId).bind()
|
||||
val availableTokens = getAvailableFeeTokens().fold(
|
||||
ifLeft = { error ->
|
||||
Timber.e("Failed to get available fee tokens: $error")
|
||||
if (selectedToken.currency !is CryptoCurrency.Coin) {
|
||||
raise(error)
|
||||
}
|
||||
emptyList()
|
||||
},
|
||||
ifRight = { it },
|
||||
)
|
||||
LoadedFeeResult.Extended(
|
||||
fee = fee,
|
||||
selectedToken = getSelectedTokenStatus(fee.feeTokenId).bind(),
|
||||
availableTokens = getAvailableFeeTokens().bind(),
|
||||
selectedToken = selectedToken,
|
||||
availableTokens = availableTokens,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,8 @@ internal class FeeSelectorModel @Inject constructor(
|
|||
CommonSendAnalyticEvents.FeeSummaryScreenOpened(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
source = params.analyticsSendSource,
|
||||
blockchain = params.cryptoCurrencyStatus.currency.network.name,
|
||||
token = params.cryptoCurrencyStatus.currency.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -95,6 +97,8 @@ internal class FeeSelectorModel @Inject constructor(
|
|||
CommonSendAnalyticEvents.FeeScreenOpened(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
source = params.analyticsSendSource,
|
||||
blockchain = params.cryptoCurrencyStatus.currency.network.name,
|
||||
token = params.cryptoCurrencyStatus.currency.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -105,6 +109,7 @@ internal class FeeSelectorModel @Inject constructor(
|
|||
categoryName = params.analyticsCategoryName,
|
||||
source = params.analyticsSendSource,
|
||||
availableTokens = getAvailableTokensString(),
|
||||
blockchain = params.cryptoCurrencyStatus.currency.network.name,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsParam
|
|||
import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_FROM
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.ENS_ADDRESS
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TOKEN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.NONCE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
|
||||
|
|
@ -42,6 +43,7 @@ internal sealed class SendAnalyticEvents(
|
|||
AnalyticsParam.EmptyFull.Full -> true.toString().capitalize()
|
||||
}
|
||||
put(ENS_ADDRESS, ensAddress)
|
||||
put(FEE_TOKEN, feeToken)
|
||||
},
|
||||
), AppsFlyerIncludedEvent
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ package com.tangem.feature.swap.analytics
|
|||
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TOKEN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_TOKEN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_TOKEN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
|
||||
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||
import com.tangem.feature.swap.domain.models.ui.FeeType
|
||||
|
|
@ -20,9 +22,15 @@ sealed class SwapEvents(
|
|||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent(SWAP_CATEGORY, event, params) {
|
||||
|
||||
data class SwapScreenOpened(val token: String) : SwapEvents(
|
||||
data class SwapScreenOpened(
|
||||
val token: String,
|
||||
val blockchain: String,
|
||||
) : SwapEvents(
|
||||
event = "Swap Screen Opened",
|
||||
params = mapOf("Token" to token),
|
||||
params = mapOf(
|
||||
TOKEN_PARAM to token,
|
||||
BLOCKCHAIN to blockchain,
|
||||
),
|
||||
), AppsFlyerIncludedEvent
|
||||
|
||||
class SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked")
|
||||
|
|
|
|||
|
|
@ -330,7 +330,12 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
analyticsEventHandler.send(SwapEvents.SwapScreenOpened(initialCurrencyFrom.symbol))
|
||||
analyticsEventHandler.send(
|
||||
SwapEvents.SwapScreenOpened(
|
||||
token = initialCurrencyFrom.symbol,
|
||||
blockchain = initialCurrencyFrom.network.name,
|
||||
),
|
||||
)
|
||||
|
||||
getBalanceHidingSettingsUseCase()
|
||||
.onEach { settings ->
|
||||
|
|
|
|||
|
|
@ -2,5 +2,4 @@ package com.tangem.features.tangempay
|
|||
|
||||
interface TangemPayFeatureToggles {
|
||||
val isTangemPayEnabled: Boolean
|
||||
val isEntryPointsEnabled: Boolean
|
||||
}
|
||||
|
|
@ -7,6 +7,4 @@ internal class DefaultTangemPayFeatureToggles(
|
|||
) : TangemPayFeatureToggles {
|
||||
override val isTangemPayEnabled
|
||||
get() = featureTogglesManager.isFeatureEnabled("TANGEM_PAY_ENABLED")
|
||||
override val isEntryPointsEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("TANGEM_PAY_ENTRYPOINT_ENABLED")
|
||||
}
|
||||
|
|
@ -17,10 +17,17 @@ internal data class TangemPayTxHistoryDetailsUM(
|
|||
val transactionAmountColor: ColorReference,
|
||||
val localTransactionText: String?,
|
||||
val labelState: LabelUM?,
|
||||
val notification: NotificationConfig?,
|
||||
val notification: NotificationState?,
|
||||
val buttons: ImmutableList<ButtonState>,
|
||||
val dismiss: () -> Unit,
|
||||
) {
|
||||
|
||||
data class NotificationState(
|
||||
val config: NotificationConfig,
|
||||
val titleColor: ColorReference,
|
||||
val iconTint: ColorReference,
|
||||
val containerColor: ColorReference?,
|
||||
)
|
||||
|
||||
data class ButtonState(val text: TextReference, val onClick: () -> Unit, val startIcon: ImageReference.Res? = null)
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.features.tangempay.model.transformers
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal object PayDetailsSpendSubtitleConverter : Converter<TangemPayTxHistoryItem.Spend, TextReference> {
|
||||
|
||||
override fun convert(value: TangemPayTxHistoryItem.Spend): TextReference {
|
||||
val merchantCategory = value.merchantCategory
|
||||
val enrichedMerchantCategory = value.enrichedMerchantCategory
|
||||
val merchantCategoryCode = value.merchantCategoryCode
|
||||
|
||||
val categoryCodeText = if (!merchantCategoryCode.isNullOrEmpty()) {
|
||||
resourceReference(
|
||||
id = R.string.tangem_pay_history_item_spend_mcc,
|
||||
formatArgs = wrappedList(merchantCategoryCode),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val categoryText = when {
|
||||
// if merchantCategory isNotEmpty use this
|
||||
!merchantCategory.isNullOrEmpty() -> stringReference(merchantCategory)
|
||||
|
||||
// If merchantCategory empty or null but enrichedMerchantCategory is not empty
|
||||
!enrichedMerchantCategory.isNullOrEmpty() -> stringReference(enrichedMerchantCategory)
|
||||
|
||||
else -> resourceReference(R.string.tangem_pay_other)
|
||||
}
|
||||
|
||||
return if (categoryCodeText != null) {
|
||||
resourceReference(
|
||||
id = R.string.tangem_pay_history_item_spend_mc_title_format,
|
||||
formatArgs = wrappedList(categoryText, categoryCodeText),
|
||||
)
|
||||
} else {
|
||||
categoryText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import com.tangem.core.ui.extensions.*
|
|||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.price
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
|
|
@ -24,7 +25,7 @@ internal object TangemPayTxHistoryDetailsConverter :
|
|||
Converter<TangemPayTxHistoryDetailsConverter.Input, TangemPayTxHistoryDetailsUM> {
|
||||
private val dateFormatter = DateTimeFormatters.getBestFormatterBySkeleton("dd MMMM")
|
||||
|
||||
private val paySpendSubtitleConverter = PaySpendSubtitleConverter
|
||||
private val paySpendSubtitleConverter = PayDetailsSpendSubtitleConverter
|
||||
|
||||
override fun convert(value: Input): TangemPayTxHistoryDetailsUM {
|
||||
val transaction = value.item
|
||||
|
|
@ -140,13 +141,16 @@ internal object TangemPayTxHistoryDetailsConverter :
|
|||
private fun TangemPayTxHistoryItem.extractAmountColor(): ColorReference {
|
||||
return when (this) {
|
||||
is TangemPayTxHistoryItem.Fee,
|
||||
is TangemPayTxHistoryItem.Spend,
|
||||
is TangemPayTxHistoryItem.Payment,
|
||||
-> themedColor { TangemTheme.colors.text.primary1 }
|
||||
is TangemPayTxHistoryItem.Collateral -> when (this.type) {
|
||||
TangemPayTxHistoryItem.Type.Deposit -> themedColor { TangemTheme.colors.text.accent }
|
||||
TangemPayTxHistoryItem.Type.Withdrawal -> themedColor { TangemTheme.colors.text.primary1 }
|
||||
}
|
||||
is TangemPayTxHistoryItem.Spend -> when (this.status) {
|
||||
TangemPayTxHistoryItem.Status.DECLINED -> themedColor { TangemTheme.colors.text.warning }
|
||||
else -> themedColor { TangemTheme.colors.text.primary1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -200,21 +204,39 @@ internal object TangemPayTxHistoryDetailsConverter :
|
|||
}
|
||||
}
|
||||
|
||||
private fun TangemPayTxHistoryItem.extractNotification(): NotificationConfig? {
|
||||
private fun TangemPayTxHistoryItem.extractNotification(): TangemPayTxHistoryDetailsUM.NotificationState? {
|
||||
return when (this) {
|
||||
is TangemPayTxHistoryItem.Payment -> null
|
||||
is TangemPayTxHistoryItem.Collateral -> null
|
||||
is TangemPayTxHistoryItem.Fee -> NotificationConfig(
|
||||
title = resourceReference(R.string.tangem_pay_transaction_fee_notification_text),
|
||||
subtitle = TextReference.EMPTY,
|
||||
iconResId = R.drawable.ic_token_info_24,
|
||||
)
|
||||
is TangemPayTxHistoryItem.Spend -> when (this.status) {
|
||||
TangemPayTxHistoryItem.Status.DECLINED -> NotificationConfig(
|
||||
title = resourceReference(R.string.tangem_pay_transaction_declined_notification_text),
|
||||
is TangemPayTxHistoryItem.Fee -> TangemPayTxHistoryDetailsUM.NotificationState(
|
||||
config = NotificationConfig(
|
||||
title = resourceReference(R.string.tangem_pay_transaction_fee_notification_text),
|
||||
subtitle = TextReference.EMPTY,
|
||||
iconResId = R.drawable.ic_token_info_24,
|
||||
)
|
||||
),
|
||||
titleColor = themedColor { TangemTheme.colors.text.tertiary },
|
||||
iconTint = themedColor { TangemTheme.colors.icon.secondary },
|
||||
containerColor = null,
|
||||
)
|
||||
is TangemPayTxHistoryItem.Spend -> when (this.status) {
|
||||
TangemPayTxHistoryItem.Status.DECLINED ->
|
||||
TangemPayTxHistoryDetailsUM.NotificationState(
|
||||
config = NotificationConfig(
|
||||
title = if (declinedReason.isNullOrEmpty()) {
|
||||
resourceReference(R.string.tangem_pay_transaction_declined_notification_text)
|
||||
} else {
|
||||
resourceReference(
|
||||
id = R.string.tangem_pay_history_item_spend_mc_declined_reason,
|
||||
formatArgs = wrappedList(requireNotNull(declinedReason)),
|
||||
)
|
||||
},
|
||||
subtitle = TextReference.EMPTY,
|
||||
iconResId = R.drawable.ic_token_info_24,
|
||||
),
|
||||
titleColor = themedColor { TangemTheme.colors.text.warning },
|
||||
iconTint = themedColor { TangemTheme.colors.icon.warning },
|
||||
containerColor = themedColor { TangemColorPalette.Amaranth.copy(alpha = 0.1F) },
|
||||
)
|
||||
TangemPayTxHistoryItem.Status.PENDING,
|
||||
TangemPayTxHistoryItem.Status.COMPLETED,
|
||||
TangemPayTxHistoryItem.Status.RESERVED,
|
||||
|
|
|
|||
|
|
@ -37,10 +37,10 @@ internal class TangemPayTxHistoryItemsConverter(
|
|||
val localDate = spend.date.withZone(DateTimeZone.getDefault())
|
||||
val amountPrefix = when {
|
||||
spend.amount.isZero() -> ""
|
||||
spend.status == TangemPayTxHistoryItem.Status.DECLINED -> ""
|
||||
else -> StringsSigns.MINUS
|
||||
spend.status == TangemPayTxHistoryItem.Status.DECLINED || spend.amount.isPositive() -> StringsSigns.MINUS
|
||||
else -> StringsSigns.PLUS
|
||||
}
|
||||
val amount = amountPrefix + spend.amount.format {
|
||||
val amount = amountPrefix + spend.amount.abs().format {
|
||||
fiat(fiatCurrencyCode = spend.currency.currencyCode, fiatCurrencySymbol = spend.currency.symbol)
|
||||
}
|
||||
return TangemPayTransactionState.Content.Spend(
|
||||
|
|
@ -48,8 +48,9 @@ internal class TangemPayTxHistoryItemsConverter(
|
|||
onClick = { txHistoryUiActions.onTransactionClick(spend) },
|
||||
amount = amount,
|
||||
amountColor = themedColor {
|
||||
when (spend.status) {
|
||||
TangemPayTxHistoryItem.Status.DECLINED -> TangemTheme.colors.text.warning
|
||||
when {
|
||||
spend.status == TangemPayTxHistoryItem.Status.DECLINED -> TangemTheme.colors.text.warning
|
||||
amountPrefix == StringsSigns.PLUS -> TangemTheme.colors.text.accent
|
||||
else -> TangemTheme.colors.text.primary1
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import com.tangem.core.ui.components.label.entity.LabelUM
|
|||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
|
|
@ -83,21 +84,24 @@ internal fun TangemPayTxHistoryDetailsContent(state: TangemPayTxHistoryDetailsUM
|
|||
style = TangemTheme.typography.head,
|
||||
color = state.transactionAmountColor.resolveReference(),
|
||||
)
|
||||
state.localTransactionText?.let { localTransaction ->
|
||||
if (state.localTransactionText != null) {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
text = localTransaction.orMaskWithStars(state.isBalanceHidden),
|
||||
text = state.localTransactionText.orMaskWithStars(state.isBalanceHidden),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
state.labelState?.let { Label(state = state.labelState, modifier = Modifier.padding(top = 12.dp)) }
|
||||
if (state.labelState != null) {
|
||||
Label(state = state.labelState, modifier = Modifier.padding(top = 12.dp))
|
||||
}
|
||||
SpacerH32()
|
||||
state.notification?.let {
|
||||
if (state.notification != null) {
|
||||
Notification(
|
||||
config = state.notification,
|
||||
titleColor = TangemTheme.colors.text.tertiary,
|
||||
iconTint = TangemTheme.colors.icon.secondary,
|
||||
config = state.notification.config,
|
||||
titleColor = state.notification.titleColor.resolveReference(),
|
||||
iconTint = state.notification.iconTint.resolveReference(),
|
||||
containerColor = state.notification.containerColor?.resolveReference(),
|
||||
)
|
||||
}
|
||||
ButtonsContainer(
|
||||
|
|
@ -195,10 +199,15 @@ private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterPr
|
|||
text = resourceReference(R.string.tangem_pay_status_declined),
|
||||
style = LabelStyle.WARNING,
|
||||
),
|
||||
notification = NotificationConfig(
|
||||
title = stringReference("The bank rejected this transaction request."),
|
||||
subtitle = TextReference.EMPTY,
|
||||
iconResId = R.drawable.ic_token_info_24,
|
||||
notification = TangemPayTxHistoryDetailsUM.NotificationState(
|
||||
config = NotificationConfig(
|
||||
title = stringReference("The bank rejected this transaction request."),
|
||||
subtitle = TextReference.EMPTY,
|
||||
iconResId = R.drawable.ic_token_info_24,
|
||||
),
|
||||
titleColor = themedColor { TangemTheme.colors.text.warning },
|
||||
iconTint = themedColor { TangemTheme.colors.icon.warning },
|
||||
containerColor = themedColor { TangemColorPalette.Amaranth.copy(alpha = 0.1F) },
|
||||
),
|
||||
buttons = persistentListOf(
|
||||
TangemPayTxHistoryDetailsUM.ButtonState(
|
||||
|
|
@ -240,10 +249,15 @@ private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterPr
|
|||
transactionAmountColor = themedColor { TangemTheme.colors.text.primary1 },
|
||||
localTransactionText = null,
|
||||
labelState = null,
|
||||
notification = NotificationConfig(
|
||||
title = stringReference("This fee goes to cover the cost of handling your transfer."),
|
||||
subtitle = TextReference.EMPTY,
|
||||
iconResId = R.drawable.ic_token_info_24,
|
||||
notification = TangemPayTxHistoryDetailsUM.NotificationState(
|
||||
config = NotificationConfig(
|
||||
title = stringReference("This fee goes to cover the cost of handling your transfer."),
|
||||
subtitle = TextReference.EMPTY,
|
||||
iconResId = R.drawable.ic_token_info_24,
|
||||
),
|
||||
titleColor = themedColor { TangemTheme.colors.text.tertiary },
|
||||
iconTint = themedColor { TangemTheme.colors.icon.secondary },
|
||||
containerColor = null,
|
||||
),
|
||||
buttons = persistentListOf(
|
||||
TangemPayTxHistoryDetailsUM.ButtonState(
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.MainScreenTestTags
|
||||
import com.tangem.core.ui.test.MarketTooltipTestTags
|
||||
import com.tangem.core.ui.utils.TangemSharedTransitionLayout
|
||||
import com.tangem.core.ui.utils.lineTo
|
||||
import com.tangem.core.ui.utils.moveTo
|
||||
import com.tangem.core.ui.utils.toPx
|
||||
|
|
@ -180,79 +181,81 @@ private fun WalletContent(
|
|||
)
|
||||
} ?: PaddingValues(bottom = TangemTheme.dimens.spacing92 + bottomBarHeight)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.testTag(MainScreenTestTags.SCREEN_CONTAINER),
|
||||
state = listState,
|
||||
contentPadding = contentPadding,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
item(
|
||||
// !!! Type of the key should be saveable via Bundle on Android !!!
|
||||
key = state.wallets.map { it.walletCardState.id.stringValue },
|
||||
contentType = state.wallets.map { it.walletCardState.id },
|
||||
TangemSharedTransitionLayout {
|
||||
LazyColumn(
|
||||
modifier = Modifier.testTag(MainScreenTestTags.SCREEN_CONTAINER),
|
||||
state = listState,
|
||||
contentPadding = contentPadding,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
WalletsList(
|
||||
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
||||
lazyListState = walletsListState,
|
||||
wallets = state.wallets.map(WalletState::walletCardState).toImmutableList(),
|
||||
isBalanceHidden = state.isHidingMode,
|
||||
)
|
||||
}
|
||||
|
||||
when (selectedWallet) {
|
||||
is WalletState.MultiCurrency -> {
|
||||
actions(
|
||||
actions = selectedWallet.buttons,
|
||||
selectedWalletIndex = selectedWalletIndex,
|
||||
modifier = movableItemModifier.padding(top = betweenItemsPadding),
|
||||
)
|
||||
}
|
||||
is WalletState.SingleCurrency -> {
|
||||
lazyActions(
|
||||
actions = selectedWallet.buttons,
|
||||
selectedWalletIndex = selectedWalletIndex,
|
||||
modifier = movableItemModifier.padding(top = betweenItemsPadding),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
notifications(configs = selectedWallet.warnings, modifier = itemModifier)
|
||||
|
||||
if (selectedWallet is WalletState.MultiCurrency) {
|
||||
item(
|
||||
key = "TangemPayMainScreenBlock",
|
||||
contentType = selectedWallet.tangemPayState::class.java,
|
||||
// !!! Type of the key should be saveable via Bundle on Android !!!
|
||||
key = state.wallets.map { it.walletCardState.id.stringValue },
|
||||
contentType = state.wallets.map { it.walletCardState.id },
|
||||
) {
|
||||
TangemPayMainScreenBlock(
|
||||
state = selectedWallet.tangemPayState,
|
||||
WalletsList(
|
||||
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
||||
lazyListState = walletsListState,
|
||||
wallets = state.wallets.map(WalletState::walletCardState).toImmutableList(),
|
||||
isBalanceHidden = state.isHidingMode,
|
||||
modifier = itemModifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
(selectedWallet as? WalletState.SingleCurrency)?.let { walletState ->
|
||||
walletState.marketPriceBlockState?.let { marketPriceBlockState ->
|
||||
marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier)
|
||||
when (selectedWallet) {
|
||||
is WalletState.MultiCurrency -> {
|
||||
actions(
|
||||
actions = selectedWallet.buttons,
|
||||
selectedWalletIndex = selectedWalletIndex,
|
||||
modifier = movableItemModifier.padding(top = betweenItemsPadding),
|
||||
)
|
||||
}
|
||||
is WalletState.SingleCurrency -> {
|
||||
lazyActions(
|
||||
actions = selectedWallet.buttons,
|
||||
selectedWalletIndex = selectedWalletIndex,
|
||||
modifier = movableItemModifier.padding(top = betweenItemsPadding),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (walletState is WalletState.SingleCurrency.Content) {
|
||||
expressTransactionsItems(
|
||||
expressTxs = walletState.expressTxsToDisplay,
|
||||
modifier = itemModifier,
|
||||
)
|
||||
|
||||
notifications(configs = selectedWallet.warnings, modifier = itemModifier)
|
||||
|
||||
if (selectedWallet is WalletState.MultiCurrency) {
|
||||
item(
|
||||
key = "TangemPayMainScreenBlock",
|
||||
contentType = selectedWallet.tangemPayState::class.java,
|
||||
) {
|
||||
TangemPayMainScreenBlock(
|
||||
state = selectedWallet.tangemPayState,
|
||||
isBalanceHidden = state.isHidingMode,
|
||||
modifier = itemModifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
(selectedWallet as? WalletState.SingleCurrency)?.let { walletState ->
|
||||
walletState.marketPriceBlockState?.let { marketPriceBlockState ->
|
||||
marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier)
|
||||
}
|
||||
if (walletState is WalletState.SingleCurrency.Content) {
|
||||
expressTransactionsItems(
|
||||
expressTxs = walletState.expressTxsToDisplay,
|
||||
modifier = itemModifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
contentItems(
|
||||
state = selectedWallet,
|
||||
txHistoryItems = txHistoryItems,
|
||||
isBalanceHidden = state.isHidingMode,
|
||||
modifier = movableItemModifier,
|
||||
)
|
||||
|
||||
nftCollections(state = selectedWallet, itemModifier = itemModifier)
|
||||
|
||||
organizeTokens(state = selectedWallet, itemModifier = itemModifier)
|
||||
}
|
||||
|
||||
contentItems(
|
||||
state = selectedWallet,
|
||||
txHistoryItems = txHistoryItems,
|
||||
isBalanceHidden = state.isHidingMode,
|
||||
modifier = movableItemModifier,
|
||||
)
|
||||
|
||||
nftCollections(state = selectedWallet, itemModifier = itemModifier)
|
||||
|
||||
organizeTokens(state = selectedWallet, itemModifier = itemModifier)
|
||||
}
|
||||
|
||||
ShowBottomSheet(bottomSheetConfig = selectedWallet.bottomSheetConfig)
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ internal fun LazyListScope.portfolioTokensList(
|
|||
}
|
||||
itemsIndexed(
|
||||
items = tokens,
|
||||
key = { _, item -> item.id },
|
||||
key = { _, item -> item.id.toString() + "-portfolio-${portfolio.id}" },
|
||||
contentType = { _, item -> item::class.java },
|
||||
itemContent = { tokenIndex, token ->
|
||||
val indexWithHeader = tokenIndex.inc()
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ import com.tangem.features.yield.supply.impl.R
|
|||
import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM
|
||||
import com.tangem.features.yield.supply.impl.main.model.transformers.YieldSupplyTokenStatusSuccessTransformer
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -57,6 +55,7 @@ internal class YieldSupplyModel @Inject constructor(
|
|||
private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase,
|
||||
private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase,
|
||||
private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase,
|
||||
private val yieldSupplyEnterStatusFlowUseCase: YieldSupplyEnterStatusFlowUseCase,
|
||||
private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase,
|
||||
private val yieldSupplyGetDustMinAmountUseCase: YieldSupplyGetDustMinAmountUseCase,
|
||||
) : Model(), YieldSupplyClickIntents {
|
||||
|
|
@ -71,8 +70,6 @@ internal class YieldSupplyModel @Inject constructor(
|
|||
var userWallet: UserWallet by Delegates.notNull()
|
||||
private var latestCryptoCurrencyStatus: CryptoCurrencyStatus? = null
|
||||
|
||||
private val loadStatusJobHolder = JobHolder()
|
||||
|
||||
private val isFirstCryptoCurrencyStatusEmission = AtomicBoolean(true)
|
||||
|
||||
init {
|
||||
|
|
@ -84,7 +81,7 @@ internal class YieldSupplyModel @Inject constructor(
|
|||
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
|
||||
val isAvailable = yieldSupplyIsAvailableUseCase(params.userWalletId, params.cryptoCurrency)
|
||||
if (isAvailable) {
|
||||
subscribeOnCurrencyStatusUpdates()
|
||||
loadUserWalletData()
|
||||
singleNetworkStatusFetcher(
|
||||
params = SingleNetworkStatusFetcher.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
|
|
@ -95,30 +92,12 @@ internal class YieldSupplyModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun subscribeOnCurrencyStatusUpdates() {
|
||||
private fun loadUserWalletData() {
|
||||
modelScope.launch {
|
||||
getUserWalletUseCase(params.userWalletId).fold(
|
||||
ifRight = { wallet ->
|
||||
userWallet = wallet
|
||||
|
||||
getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
|
||||
userWalletId = params.userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
isSingleWalletWithTokens = false,
|
||||
).onEach { maybeCryptoCurrency ->
|
||||
maybeCryptoCurrency.fold(
|
||||
ifRight = { cryptoCurrencyStatus ->
|
||||
latestCryptoCurrencyStatus = cryptoCurrencyStatus
|
||||
if (isFirstCryptoCurrencyStatusEmission.compareAndSet(true, false)) {
|
||||
sendInfoAboutProtocolStatus(cryptoCurrencyStatus)
|
||||
}
|
||||
onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus)
|
||||
},
|
||||
ifLeft = {
|
||||
Timber.w(it.toString())
|
||||
},
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
subscribeOnCurrencyStatusUpdates()
|
||||
},
|
||||
ifLeft = {
|
||||
Timber.w(it.toString())
|
||||
|
|
@ -128,6 +107,36 @@ internal class YieldSupplyModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun subscribeOnCurrencyStatusUpdates() {
|
||||
combine(
|
||||
getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
|
||||
userWalletId = params.userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
isSingleWalletWithTokens = false,
|
||||
),
|
||||
yieldSupplyEnterStatusFlowUseCase(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
),
|
||||
) { maybeCryptoCurrency, _ ->
|
||||
maybeCryptoCurrency
|
||||
}.flowOn(dispatchers.io)
|
||||
.onEach { maybeCryptoCurrency ->
|
||||
maybeCryptoCurrency.fold(
|
||||
ifRight = { cryptoCurrencyStatus ->
|
||||
latestCryptoCurrencyStatus = cryptoCurrencyStatus
|
||||
if (isFirstCryptoCurrencyStatusEmission.compareAndSet(true, false)) {
|
||||
sendInfoAboutProtocolStatus(cryptoCurrencyStatus)
|
||||
}
|
||||
onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus)
|
||||
},
|
||||
ifLeft = {
|
||||
Timber.w(it.toString())
|
||||
},
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private suspend fun loadTokenStatus() {
|
||||
val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return
|
||||
yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken)
|
||||
|
|
@ -168,13 +177,11 @@ internal class YieldSupplyModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) = modelScope.launch(
|
||||
dispatchers.default,
|
||||
) {
|
||||
private suspend fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) {
|
||||
val isCryptoCurrencyStatusFromCache = cryptoCurrencyStatus.value.sources.networkSource != StatusSource.ACTUAL
|
||||
val processing = uiState.value is YieldSupplyUM.Processing
|
||||
if (isCryptoCurrencyStatusFromCache && processing) {
|
||||
return@launch
|
||||
return
|
||||
}
|
||||
|
||||
val pendingStatus = yieldSupplyEnterStatusUseCase(
|
||||
|
|
@ -187,7 +194,7 @@ internal class YieldSupplyModel @Inject constructor(
|
|||
} else {
|
||||
loadStatus(cryptoCurrencyStatus)
|
||||
}
|
||||
}.saveIn(loadStatusJobHolder)
|
||||
}
|
||||
|
||||
private fun showProcessing(status: YieldSupplyPendingStatus) {
|
||||
uiState.update {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit cfc2c0ab47e2e821f38f463f32f120d620804c72
|
||||
Subproject commit ab6aee1d49ac0cfca644330a525dced725d5a257
|
||||
Loading…
Add table
Add a link
Reference in a new issue